// Copyright 2024 Matthew Rich . All rights reserved. // Container resource package resource import ( "context" "fmt" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/image" "github.com/docker/docker/client" "gopkg.in/yaml.v3" _ "gopkg.in/yaml.v3" "log/slog" "net/url" _ "os" _ "os/exec" "strings" "encoding/json" "io" "gitea.rosskeen.house/rosskeen.house/machine" "decl/internal/codec" ) type ContainerImageClient interface { ImagePull(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error) ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) ImageRemove(ctx context.Context, imageID string, options image.RemoveOptions) ([]image.DeleteResponse, error) ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) Close() error } type ContainerImage struct { stater machine.Stater `yaml:"-" json:"-"` Id string `json:"id,omitempty" yaml:"id,omitempty"` Name string `json:"name" yaml:"name"` Created string `json:"created,omitempty" yaml:"created,omitempty"` Architecture string `json:"architecture,omitempty" yaml:"architecture,omitempty"` Variant string `json:"variant,omitempty" yaml:"variant,omitempty"` OS string `json:"os" yaml:"os"` Size int64 `json:"size" yaml:"size"` Author string `json:"author,omitempty" yaml:"author,omitempty"` Comment string `json:"comment,omitempty" yaml:"comment,omitempty"` Dockerfile string `json:"dockerfile,omitempty" yaml:"dockerfile,omitempty"` ContextRef ResourceReference `json:"contextref,omitempty" yaml:"contextref,omitempty"` InjectJX bool `json:"injectjx,omitempty" yaml:"injectjx,omitempty"` State string `yaml:"state,omitempty" json:"state,omitempty"` config ConfigurationValueGetter apiClient ContainerImageClient Resources ResourceMapper `json:"-" yaml:"-"` } func init() { ResourceTypes.Register([]string{"container-image"}, func(u *url.URL) Resource { c := NewContainerImage(nil) c.Name = ContainerImageNameFromURI(u) slog.Info("NewContainerImage", "container", c) return c }) } func NewContainerImage(containerClientApi ContainerImageClient) *ContainerImage { var apiClient ContainerImageClient = containerClientApi if apiClient == nil { var err error apiClient, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { panic(err) } } return &ContainerImage{ apiClient: apiClient, InjectJX: true, } } func (c *ContainerImage) SetResourceMapper(resources ResourceMapper) { c.Resources = resources } func (c *ContainerImage) Clone() Resource { return &ContainerImage { Id: c.Id, Name: c.Name, Created: c.Created, Architecture: c.Architecture, Variant: c.Variant, OS: c.OS, Size: c.Size, Author: c.Author, Comment: c.Comment, InjectJX: c.InjectJX, State: c.State, apiClient: c.apiClient, } } func (c *ContainerImage) StateMachine() machine.Stater { if c.stater == nil { c.stater = StorageMachine(c) } return c.stater } func URIFromContainerImageName(imageName string) string { var host, namespace, repo string elements := strings.Split(imageName, "/") switch len(elements) { case 1: repo = elements[0] case 2: namespace = elements[0] repo = elements[1] case 3: host = elements[0] namespace = elements[1] repo = elements[2] } if namespace == "" { return fmt.Sprintf("container-image://%s/%s", host, repo) } return fmt.Sprintf("container-image://%s/%s", host, strings.Join([]string{namespace, repo}, "/")) } // Reconstruct the image name from a given parsed URL func ContainerImageNameFromURI(u *url.URL) string { var host string = u.Hostname() // var host, namespace, repo string elements := strings.FieldsFunc(u.RequestURI(), func(c rune) bool { return c == '/' }) slog.Info("ContainerImageNameFromURI", "url", u, "elements", elements) /* switch len(elements) { case 1: repo = elements[0] case 2: namespace = elements[0] repo = elements[1] } */ if host == "" { return strings.Join(elements, "/") } return fmt.Sprintf("%s/%s", host, strings.Join(elements, "/")) } func (c *ContainerImage) URI() string { return URIFromContainerImageName(c.Name) } func (c *ContainerImage) SetURI(uri string) error { resourceUri, e := url.Parse(uri) if e == nil { if resourceUri.Scheme == c.Type() { c.Name = strings.Join([]string{resourceUri.Hostname(), resourceUri.RequestURI()}, ":") } else { e = fmt.Errorf("%w: %s is not a %s", ErrInvalidResourceURI, uri, c.Type()) } } return e } func (c *ContainerImage) UseConfig(config ConfigurationValueGetter) { c.config = config } func (c *ContainerImage) JSON() ([]byte, error) { return json.Marshal(c) } func (c *ContainerImage) Validate() error { return fmt.Errorf("failed") } func (c *ContainerImage) Notify(m *machine.EventMessage) { slog.Info("ContainerImage.Notify()", "event", m, "state", c.State) ctx := context.Background() switch m.On { case machine.ENTERSTATEEVENT: switch m.Dest { case "start_read": if _,readErr := c.Read(ctx); readErr == nil { if triggerErr := c.stater.Trigger("state_read"); triggerErr == nil { return } else { c.State = "absent" panic(triggerErr) } } else { c.State = "absent" panic(readErr) } case "start_create": if createErr := c.Create(ctx); createErr == nil { if triggerErr := c.stater.Trigger("created"); triggerErr == nil { return } else { c.State = "absent" panic(triggerErr) } } else { c.State = "absent" panic(createErr) } case "start_update": if createErr := c.Update(ctx); createErr == nil { if triggerErr := c.stater.Trigger("updated"); triggerErr == nil { return } else { c.State = "absent" } } else { c.State = "absent" panic(createErr) } case "start_delete": if deleteErr := c.Delete(ctx); deleteErr == nil { if triggerErr := c.StateMachine().Trigger("deleted"); triggerErr == nil { return } else { c.State = "present" panic(triggerErr) } } else { c.State = "present" panic(deleteErr) } case "present", "created", "read": c.State = "present" case "absent": c.State = "absent" } case machine.EXITSTATEEVENT: } } func (c *ContainerImage) Apply() error { ctx := context.Background() switch c.State { case "absent": return c.Delete(ctx) case "present": return c.Create(ctx) } return nil } func (c *ContainerImage) Load(r io.Reader) error { return codec.NewYAMLDecoder(r).Decode(c) } func (c *ContainerImage) LoadDecl(yamlResourceDeclaration string) error { return codec.NewYAMLStringDecoder(yamlResourceDeclaration).Decode(c) } func (c *ContainerImage) Create(ctx context.Context) error { buildOptions := types.ImageBuildOptions{ Dockerfile: c.Dockerfile, Tags: []string{c.Name}, } if c.ContextRef.Exists() { if c.ContextRef.ContentType() == "tar" { ref := c.ContextRef.Lookup(c.Resources) reader, readerErr := ref.ContentReaderStream() if readerErr != nil { return readerErr } buildResponse, buildErr := c.apiClient.ImageBuild(ctx, reader, buildOptions) if buildErr != nil { return buildErr } defer buildResponse.Body.Close() if _, outputErr := io.ReadAll(buildResponse.Body); outputErr != nil { return fmt.Errorf("%w %s %s", outputErr, c.Type(), c.Name) } } } return nil } func (c *ContainerImage) Update(ctx context.Context) error { return c.Create(ctx) } func (c *ContainerImage) Pull(ctx context.Context) error { out, err := c.apiClient.ImagePull(ctx, c.Name, image.PullOptions{}) slog.Info("ContainerImage.Pull()", "name", c.Name, "error", err) if err == nil { if _, outputErr := io.ReadAll(out); outputErr != nil { return fmt.Errorf("%w: %s %s", outputErr, c.Type(), c.Name) } } else { return fmt.Errorf("%w: %s %s", err, c.Type(), c.Name) } return nil } func (c *ContainerImage) Inspect(ctx context.Context) (imageInspect types.ImageInspect) { var err error imageInspect, _, err = c.apiClient.ImageInspectWithRaw(ctx, c.Name) if err != nil { panic(err) } return } func (c *ContainerImage) Read(ctx context.Context) (resourceYaml []byte, err error) { defer func() { if r := recover(); r != nil { c.State = "absent" resourceYaml = nil err = fmt.Errorf("%w", r.(error)) } }() var imageInspect types.ImageInspect imageInspect, _, err = c.apiClient.ImageInspectWithRaw(ctx, c.Name) slog.Info("ContainerImage.Read()", "name", c.Name, "error", err) if err != nil { if client.IsErrNotFound(err) { if pullErr := c.Pull(ctx); pullErr != nil { panic(pullErr) } imageInspect = c.Inspect(ctx) } else { panic(err) } } c.State = "present" c.Id = imageInspect.ID /* if c.Name == "" { c.Name = imageInspect.Name } */ c.Created = imageInspect.Created c.Author = imageInspect.Author c.Architecture = imageInspect.Architecture c.Variant = imageInspect.Variant c.OS = imageInspect.Os c.Size = imageInspect.Size c.Comment = imageInspect.Comment slog.Info("ContainerImage.Read()", "type", c.Type(), "name", c.Name, "Id", c.Id, "state", c.State, "error", err) return yaml.Marshal(c) } func (c *ContainerImage) Delete(ctx context.Context) error { slog.Info("ContainerImage.Delete()", "image", c) options := image.RemoveOptions{ Force: false, PruneChildren: false, } _, err := c.apiClient.ImageRemove(ctx, c.Id, options) return err /* for _, img := range deletedImages { fmt.Printf("Deleted image: %s\n", img.Deleted) fmt.Printf("Untagged image: %s\n", img.Untagged) } */ } func (c *ContainerImage) Type() string { return "container-image" } func (c *ContainerImage) ResolveId(ctx context.Context) string { slog.Info("ContainerImage.ResolveId()", "name", c.Name, "machine.state", c.StateMachine().CurrentState()) imageInspect, _, err := c.apiClient.ImageInspectWithRaw(ctx, c.Name) if err != nil { triggerResult := c.StateMachine().Trigger("notexists") slog.Info("ContainerImage.ResolveId()", "name", c.Name, "machine.state", c.StateMachine().CurrentState(), "resource.state", c.State, "trigger.error", triggerResult) panic(fmt.Errorf("%w: %s %s", err, c.Type(), c.Name)) } slog.Info("ContainerImage.ResolveId()", "name", c.Name, "machine.state", c.StateMachine().CurrentState(), "resource.state", c.State) c.Id = imageInspect.ID if c.Id != "" { if triggerErr := c.StateMachine().Trigger("exists"); triggerErr != nil { panic(fmt.Errorf("%w: %s %s", triggerErr, c.Type(), c.Name)) } slog.Info("ContainerImage.ResolveId() trigger created", "machine", c.StateMachine(), "state", c.StateMachine().CurrentState()) } else { if triggerErr := c.StateMachine().Trigger("notexists"); triggerErr != nil { panic(fmt.Errorf("%w: %s %s", triggerErr, c.Type(), c.Name)) } slog.Info("ContainerImage.ResolveId()", "name", c.Name, "machine.state", c.StateMachine().CurrentState(), "resource.state", c.State) } return c.Id }