// Copyright 2024 Matthew Rich . All rights reserved. package resource import ( "context" "fmt" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/network" _ "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" "time" ) const ( ContainerNetworkTypeName TypeName = "container-network" ) type ContainerNetworkClient interface { ContainerClient NetworkCreate(ctx context.Context, name string, options network.CreateOptions) (network.CreateResponse, error) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) NetworkInspect(ctx context.Context, networkID string, options network.InspectOptions) (network.Inspect, error) } type ContainerNetwork struct { *Common `json:",inline" yaml:",inline"` stater machine.Stater `json:"-" yaml:"-"` Id string `json:"ID,omitempty" yaml:"ID,omitempty"` Name string `json:"name" yaml:"name"` Driver string `json:"driver,omitempty" yaml:"driver,omitempty"` EnableIPv6 bool `json:"enableipv6,omitempty" yaml:"enableipv6,omitempty"` Internal bool `json:"internal,omitempty" yaml:"internal,omitempty"` Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` Created time.Time `json:"created" yaml:"created"` apiClient ContainerNetworkClient Resources data.ResourceMapper `json:"-" yaml:"-"` } func init() { folio.DocumentRegistry.ResourceTypes.Register([]string{"container-network"}, func(u *url.URL) (n data.Resource) { n = NewContainerNetwork(nil) if u != nil { if err := folio.CastParsedURI(u).ConstructResource(n); err != nil { panic(err) } } return }) } func NewContainerNetwork(containerClientApi ContainerNetworkClient) (cn *ContainerNetwork) { var apiClient ContainerNetworkClient = containerClientApi if apiClient == nil { var err error apiClient, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) } } cn = &ContainerNetwork{ apiClient: apiClient, } cn.Common = NewCommon(ContainerNetworkTypeName, true) cn.Common.NormalizePath = cn.NormalizePath return cn } func (n *ContainerNetwork) Init(u data.URIParser) error { if u == nil { u = folio.URI(n.URI()).Parse() } return n.SetParsedURI(u) } func (n *ContainerNetwork) NormalizePath() error { return nil } func (n *ContainerNetwork) SetResourceMapper(resources data.ResourceMapper) { n.Resources = resources } func (n *ContainerNetwork) Clone() data.Resource { return &ContainerNetwork { Common: n.Common.Clone(), Id: n.Id, Name: n.Name, apiClient: n.apiClient, } } func (n *ContainerNetwork) StateMachine() machine.Stater { if n.stater == nil { n.stater = StorageMachine(n) } return n.stater } func (n *ContainerNetwork) Notify(m *machine.EventMessage) { ctx := context.Background() slog.Info("Notify()", "ContainerNetwork", n, "m", m) switch m.On { case machine.ENTERSTATEEVENT: switch m.Dest { case "start_read": if _,readErr := n.Read(ctx); readErr == nil { if triggerErr := n.StateMachine().Trigger("state_read"); triggerErr == nil { return } else { n.Common.State = "absent" panic(triggerErr) } } else { n.Common.State = "absent" panic(readErr) } case "start_delete": if deleteErr := n.Delete(ctx); deleteErr == nil { if triggerErr := n.StateMachine().Trigger("deleted"); triggerErr == nil { return } else { n.Common.State = "present" panic(triggerErr) } } else { n.Common.State = "present" panic(deleteErr) } case "start_create": if e := n.Create(ctx); e == nil { if triggerErr := n.StateMachine().Trigger("created"); triggerErr == nil { return } } n.Common.State = "absent" case "absent": n.Common.State = "absent" case "present", "created", "read": n.Common.State = "present" } case machine.EXITSTATEEVENT: } } func (n *ContainerNetwork) URI() string { return fmt.Sprintf("container-network://%s", n.Name) } func (n *ContainerNetwork) JSON() ([]byte, error) { return json.Marshal(n) } func (n *ContainerNetwork) Validate() error { return fmt.Errorf("failed") } func (n *ContainerNetwork) Apply() error { ctx := context.Background() switch n.Common.State { case "absent": return n.Delete(ctx) case "present": return n.Create(ctx) } return nil } func (n *ContainerNetwork) Load(docData []byte, f codec.Format) (err error) { err = f.StringDecoder(string(docData)).Decode(n) return } func (n *ContainerNetwork) LoadReader(r io.ReadCloser, f codec.Format) (err error) { err = f.Decoder(r).Decode(n) return } func (n *ContainerNetwork) LoadString(docData string, f codec.Format) (err error) { err = f.StringDecoder(docData).Decode(n) return } func (n *ContainerNetwork) LoadDecl(yamlResourceDeclaration string) error { return n.LoadString(yamlResourceDeclaration, codec.FormatYaml) } func (n *ContainerNetwork) Create(ctx context.Context) error { networkResp, err := n.apiClient.NetworkCreate(ctx, n.Name, network.CreateOptions{ Driver: "bridge", }) if err != nil { panic(err) } n.Id = networkResp.ID return nil } func (n *ContainerNetwork) Inspect(ctx context.Context, networkID string) error { networkInspect, err := n.apiClient.NetworkInspect(ctx, networkID, network.InspectOptions{}) if client.IsErrNotFound(err) { n.Common.State = "absent" } else { n.Common.State = "present" n.Id = networkInspect.ID if n.Name == "" { if networkInspect.Name[0] == '/' { n.Name = networkInspect.Name[1:] } else { n.Name = networkInspect.Name } } n.Created = networkInspect.Created n.Internal = networkInspect.Internal n.Driver = networkInspect.Driver n.Labels = networkInspect.Labels n.EnableIPv6 = networkInspect.EnableIPv6 } return nil } func (n *ContainerNetwork) Read(ctx context.Context) ([]byte, error) { var networkID string filterArgs := filters.NewArgs() filterArgs.Add("name", n.Name) networks, err := n.apiClient.NetworkList(ctx, network.ListOptions{ Filters: filterArgs, }) if err != nil { return nil, fmt.Errorf("%w: %s %s", err, n.Type(), n.Name) } for _, net := range networks { if net.Name == n.Name { networkID = net.ID } } if inspectErr := n.Inspect(ctx, networkID); inspectErr != nil { return nil, fmt.Errorf("%w: network %s", inspectErr, networkID) } slog.Info("Read() ", "type", n.Type(), "name", n.Name, "Id", n.Id) return yaml.Marshal(n) } func (n *ContainerNetwork) Update(ctx context.Context) error { return n.Create(ctx) } func (n *ContainerNetwork) Delete(ctx context.Context) error { return nil } func (n *ContainerNetwork) Type() string { return "container-network" } func (n *ContainerNetwork) ResolveId(ctx context.Context) string { filterArgs := filters.NewArgs() filterArgs.Add("name", "/"+n.Name) containers, err := n.apiClient.ContainerList(ctx, container.ListOptions{ All: true, Filters: filterArgs, }) if err != nil { panic(fmt.Errorf("%w: %s %s", err, n.Type(), n.Name)) } for _, container := range containers { for _, containerName := range container.Names { if containerName == n.Name { if n.Id == "" { n.Id = container.ID } return container.ID } } } return "" }