// Copyright 2024 Matthew Rich . All rights reserved. package resource import ( "context" "fmt" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/volume" _ "github.com/docker/docker/api/types/strslice" "github.com/docker/docker/client" "gopkg.in/yaml.v3" _ "log/slog" "net/url" _ "os" _ "os/exec" _ "strings" "encoding/json" "io" "gitea.rosskeen.house/rosskeen.house/machine" "decl/internal/codec" "decl/internal/data" "decl/internal/folio" "log/slog" ) const ( ContainerVolumeTypeName TypeName = "container-volume" ) type ContainerVolumeClient interface { ContainerClient VolumeCreate(ctx context.Context, options volume.CreateOptions) (volume.Volume, error) VolumeList(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error) VolumeInspect(ctx context.Context, volumeID string) (volume.Volume, error) VolumeRemove(ctx context.Context, volumeID string, force bool) (error) } type ContainerVolume struct { *Common `json:",inline" yaml:",inline"` stater machine.Stater `json:"-" yaml:"-"` volume.Volume `json:",inline" yaml:",inline"` apiClient ContainerVolumeClient Resources data.ResourceMapper `json:"-" yaml:"-"` } func init() { folio.DocumentRegistry.ResourceTypes.Register([]string{"container-volume"}, func(u *url.URL) (n data.Resource) { n = NewContainerVolume(nil) if u != nil { if err := folio.CastParsedURI(u).ConstructResource(n); err != nil { panic(err) } } return }) } func NewContainerVolume(containerClientApi ContainerVolumeClient) (cn *ContainerVolume) { var apiClient ContainerVolumeClient = containerClientApi if apiClient == nil { var err error apiClient, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) } } cn = &ContainerVolume{ apiClient: apiClient, } cn.Common = NewCommon(ContainerVolumeTypeName, true) cn.Common.NormalizePath = cn.NormalizePath return cn } func (v *ContainerVolume) Init(u data.URIParser) error { if u == nil { u = folio.URI(v.URI()).Parse() } return v.SetParsedURI(u) } func (v *ContainerVolume) NormalizePath() error { return nil } func (v *ContainerVolume) SetResourceMapper(resources data.ResourceMapper) { v.Resources = resources } func (v *ContainerVolume) Clone() data.Resource { return &ContainerVolume { Common: v.Common.Clone(), Volume: v.Volume, apiClient: v.apiClient, } } func (v *ContainerVolume) StateMachine() machine.Stater { if v.stater == nil { v.stater = StorageMachine(v) } return v.stater } func (v *ContainerVolume) Notify(m *machine.EventMessage) { ctx := context.Background() slog.Info("Notify()", "ContainerVolume", v, "m", m) switch m.On { case machine.ENTERSTATEEVENT: switch m.Dest { case "start_read": if _,readErr := v.Read(ctx); readErr == nil { if triggerErr := v.StateMachine().Trigger("state_read"); triggerErr == nil { return } else { v.Common.State = "absent" panic(triggerErr) } } else { v.Common.State = "absent" panic(readErr) } case "start_delete": if deleteErr := v.Delete(ctx); deleteErr == nil { if triggerErr := v.StateMachine().Trigger("deleted"); triggerErr == nil { return } else { v.Common.State = "present" panic(triggerErr) } } else { v.Common.State = "present" panic(deleteErr) } case "start_create": if e := v.Create(ctx); e == nil { if triggerErr := v.StateMachine().Trigger("created"); triggerErr == nil { return } } v.Common.State = "absent" case "absent": v.Common.State = "absent" case "present", "created", "read": v.Common.State = "present" } case machine.EXITSTATEEVENT: } } func (v *ContainerVolume) URI() string { return fmt.Sprintf("container-volume://%s", v.Name) } func (v *ContainerVolume) JSON() ([]byte, error) { return json.Marshal(v) } func (v *ContainerVolume) Validate() error { return fmt.Errorf("failed") } func (v *ContainerVolume) Apply() error { ctx := context.Background() switch v.Common.State { case "absent": return v.Delete(ctx) case "present": return v.Create(ctx) } return nil } func (v *ContainerVolume) Load(docData []byte, f codec.Format) (err error) { err = f.StringDecoder(string(docData)).Decode(v) return } func (v *ContainerVolume) LoadReader(r io.ReadCloser, f codec.Format) (err error) { err = f.Decoder(r).Decode(v) return } func (v *ContainerVolume) LoadString(docData string, f codec.Format) (err error) { err = f.StringDecoder(docData).Decode(v) return } func (n *ContainerVolume) LoadDecl(yamlResourceDeclaration string) error { return n.LoadString(yamlResourceDeclaration, codec.FormatYaml) } func (v *ContainerVolume) Create(ctx context.Context) (err error) { var spec volume.ClusterVolumeSpec if v.ClusterVolume != nil { spec = v.ClusterVolume.Spec } v.Volume, err = v.apiClient.VolumeCreate(ctx, volume.CreateOptions{ Name: v.Name, Driver: v.Driver, DriverOpts: v.Options, Labels: v.Labels, ClusterVolumeSpec: &spec, }) if err != nil { panic(err) } return nil } func (v *ContainerVolume) Inspect(ctx context.Context, volumeID string) error { volumeInspect, err := v.apiClient.VolumeInspect(ctx, volumeID) if client.IsErrNotFound(err) { v.Common.State = "absent" } else { v.Common.State = "present" v.Volume = volumeInspect if v.Name == "" { if volumeInspect.Name[0] == '/' { v.Name = volumeInspect.Name[1:] } else { v.Name = volumeInspect.Name } } } return nil } func (v *ContainerVolume) Read(ctx context.Context) ([]byte, error) { var volumeID string filterArgs := filters.NewArgs() filterArgs.Add("name", v.Name) volumes, err := v.apiClient.VolumeList(ctx, volume.ListOptions{ Filters: filterArgs, }) if err != nil { return nil, fmt.Errorf("%w: %s %s", err, v.Type(), v.Name) } for _, vol := range volumes.Volumes { if vol.Name == v.Name { volumeID = vol.Name } } if inspectErr := v.Inspect(ctx, volumeID); inspectErr != nil { return nil, fmt.Errorf("%w: volume %s", inspectErr, volumeID) } slog.Info("Read() ", "type", v.Type(), "name", v.Name) return yaml.Marshal(v) } func (v *ContainerVolume) Update(ctx context.Context) error { return v.Create(ctx) } func (v *ContainerVolume) Delete(ctx context.Context) error { return nil } func (v *ContainerVolume) Type() string { return "container-volume" } func (v *ContainerVolume) ResolveId(ctx context.Context) string { v.Inspect(ctx, v.Name) return v.Name /* volumes, err := n.apiClient.VolumeInspect(ctx, volume.ListOptions{ filterArgs := filters.NewArgs() filterArgs.Add("name", "/"+n.Name) volumes, err := n.apiClient.VolumeList(ctx, volume.ListOptions{ All: true, Filters: filterArgs, }) if err != nil { panic(fmt.Errorf("%w: %s %s", err, n.Type(), n.Name)) } for _, volume := range volumes { for _, containerName := range volume.Name { if containerName == n.Name { if n.Id == "" { n.Id = container.ID } return container.ID } } } return "" */ }