// Copyright 2024 Matthew Rich . All rights reserved. // Container resource package resource import ( "context" "fmt" _ "os" _ "gopkg.in/yaml.v3" _ "os/exec" _ "strings" "log/slog" "github.com/docker/docker/api/types/strslice" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types" "github.com/docker/docker/client" "github.com/docker/docker/api/types/network" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "gopkg.in/yaml.v3" "net/url" "path/filepath" ) type ContainerClient interface { ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error ContainerList(context.Context, types.ContainerListOptions) ([]types.Container, error) ContainerInspect(context.Context, string) (types.ContainerJSON, error) ContainerRemove(context.Context, string, container.RemoveOptions) error Close() error } type Container struct { loader YamlLoader Id string `yaml:"ID",omitempty` Name string `yaml:"name"` Path string `yaml:"path"` Cmd []string `yaml:"cmd",omitempty` Entrypoint strslice.StrSlice `yaml:"entrypoint",omitempty` Args []string `yaml:"args",omitempty` Environment map[string]string `yaml:"environment"` Image string `yaml:"image"` ResolvConfPath string `yaml:"resolvconfpath"` HostnamePath string `yaml:"hostnamepath"` HostsPath string `yaml:"hostspath"` LogPath string `yaml:"logpath"` Created string `yaml:"created"` ContainerState types.ContainerState `yaml:"containerstate"` RestartCount int `yaml:"restartcount"` Driver string `yaml:"driver"` Platform string `yaml:"platform"` MountLabel string `yaml:"mountlabel"` ProcessLabel string `yaml:"processlabel"` AppArmorProfile string `yaml:"apparmorprofile"` ExecIDs []string `yaml:"execids"` HostConfig container.HostConfig `yaml:"hostconfig"` GraphDriver types.GraphDriverData `yaml:"graphdriver"` SizeRw *int64 `json:",omitempty"` SizeRootFs *int64 `json:",omitempty"` /* Mounts []MountPoint Config *container.Config NetworkSettings *NetworkSettings */ State string `yaml:"state"` apiClient ContainerClient } func init() { ResourceTypes.Register("container", func(u *url.URL) Resource { c := NewContainer(nil) c.Name = filepath.Join(u.Hostname(), u.Path) return c }) } func NewContainer(containerClientApi ContainerClient) *Container { var apiClient ContainerClient = containerClientApi if apiClient == nil { var err error apiClient, err = client.NewClientWithOpts(client.FromEnv) if err != nil { panic(err) } } return &Container{ loader: YamlLoadDecl, apiClient: apiClient, } } func (c *Container) URI() string { return fmt.Sprintf("container://%s", c.Id) } func (c *Container) SetURI(uri string) error { resourceUri, e := url.Parse(uri) if resourceUri.Scheme == c.Type() { c.Name, e = filepath.Abs(filepath.Join(resourceUri.Hostname(), resourceUri.RequestURI())) } else { e = fmt.Errorf("%w: %s is not a %s", ErrInvalidResourceURI, uri, c.Type()) } return e } func (c *Container) Apply() error { ctx := context.Background() switch c.State { case "absent": return c.Delete(ctx) case "present": return c.Create(ctx) } return nil } func (c *Container) LoadDecl(yamlFileResourceDeclaration string) error { return c.loader(yamlFileResourceDeclaration, c) } func (c *Container) Create(ctx context.Context) error { numberOfEnvironmentVariables := len(c.Environment) config := &container.Config { Image: c.Image, Cmd: c.Cmd, Entrypoint: c.Entrypoint, Tty: false, } config.Env = make([]string, numberOfEnvironmentVariables) index := 0 for k,v := range c.Environment { config.Env[index] = k + "=" + v index++ } for i := range c.HostConfig.Mounts { if c.HostConfig.Mounts[i].Type == mount.TypeBind { if mountSourceAbsolutePath,e := filepath.Abs(c.HostConfig.Mounts[i].Source); e == nil { c.HostConfig.Mounts[i].Source = mountSourceAbsolutePath } } } resp, err := c.apiClient.ContainerCreate(ctx, config, &c.HostConfig, nil, nil, c.Name) if err != nil { panic(err) } c.Id = resp.ID /* statusCh, errCh := cli.ContainerWait(ctx, resp.ID, container.WaitConditionNotRunning) select { case err := <-errCh: if err != nil { panic(err) } case <-statusCh: } */ if startErr := c.apiClient.ContainerStart(ctx, c.Id, types.ContainerStartOptions{}); startErr != nil { return startErr } return err } // produce yaml representation of any resource func (c *Container) Read(ctx context.Context) ([]byte, error) { var containerID string filterArgs := filters.NewArgs() filterArgs.Add("name", "/" + c.Name) containers,err := c.apiClient.ContainerList(ctx, types.ContainerListOptions{ All: true, Filters: filterArgs, }) if err != nil { panic(fmt.Errorf("%w: %s %s", err, c.Type(), c.Name)) } for _, container := range containers { for _, containerName := range container.Names { if containerName == "/" + c.Name { containerID = container.ID } } } containerJSON, err := c.apiClient.ContainerInspect(ctx, containerID) if client.IsErrNotFound(err) { c.State = "absent" } else { c.State = "present" c.Id = containerJSON.ID if c.Name == "" { c.Name = containerJSON.Name } c.Path = containerJSON.Path c.Image = containerJSON.Image if containerJSON.State != nil { c.ContainerState = *containerJSON.State } c.Created = containerJSON.Created c.ResolvConfPath = containerJSON.ResolvConfPath c.HostnamePath = containerJSON.HostnamePath c.HostsPath = containerJSON.HostsPath c.LogPath = containerJSON.LogPath c.RestartCount = containerJSON.RestartCount c.Driver = containerJSON.Driver } slog.Info("Read() ", "type", c.Type(), "name", c.Name, "Id", c.Id) return yaml.Marshal(c) } func (c *Container) Delete(ctx context.Context) error { err := c.apiClient.ContainerRemove(ctx, c.Id, types.ContainerRemoveOptions{ RemoveVolumes: true, Force: false, }) if err != nil { slog.Error("Failed to remove: ", "Id", c.Id) panic(err) } return err } func (c *Container) Type() string { return "container" } func (c *Container) ResolveId(ctx context.Context) string { filterArgs := filters.NewArgs() filterArgs.Add("name", "/" + c.Name) containers,err := c.apiClient.ContainerList(ctx, types.ContainerListOptions{ All: true, Filters: filterArgs, }) if err != nil { panic(fmt.Errorf("%w: %s %s", err, c.Type(), c.Name)) } for _, container := range containers { for _, containerName := range container.Names { if containerName == c.Name { if c.Id == "" { c.Id = container.ID } return container.ID } } } return "" }