jx/internal/resource/container_image.go
Matthew Rich 5f7db0b4d0
All checks were successful
Lint / golangci-lint (push) Successful in 9m41s
Declarative Tests / test (push) Successful in 20s
fix lint errors
2024-05-26 02:14:16 -07:00

293 lines
8.0 KiB
Go

// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. 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 types.ImagePullOptions) (io.ReadCloser, error)
ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error)
ImageRemove(ctx context.Context, imageID string, options types.ImageRemoveOptions) ([]image.DeleteResponse, 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"`
State string `yaml:"state,omitempty" json:"state,omitempty"`
apiClient ContainerImageClient
}
func init() {
ResourceTypes.Register("container-image", func(u *url.URL) Resource {
c := NewContainerImage(nil)
c.Name = strings.Join([]string{u.Hostname(), u.Path}, ":")
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,
}
}
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,
State: c.State,
apiClient: c.apiClient,
}
}
func (c *ContainerImage) StateMachine() machine.Stater {
if c.stater == nil {
c.stater = StorageMachine(c)
}
return c.stater
}
func (c *ContainerImage) URI() string {
var host, namespace, repo string
elements := strings.Split(c.Name, "/")
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}, "/"))
}
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) 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_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 {
return nil
}
func (c *ContainerImage) Read(ctx context.Context) ([]byte, error) {
out, err := c.apiClient.ImagePull(ctx, c.Name, types.ImagePullOptions{})
slog.Info("Read()", "name", c.Name, "error", err)
_, outputErr := io.ReadAll(out)
if err != nil {
return nil, fmt.Errorf("%w: %s %s", err, c.Type(), c.Name)
}
if outputErr != nil {
return nil, fmt.Errorf("%w: %s %s", outputErr, c.Type(), c.Name)
}
imageInspect, _, err := c.apiClient.ImageInspectWithRaw(ctx, c.Name)
if err != nil {
if client.IsErrNotFound(err) {
slog.Info("ContainerImage.Read()", "oldstate", c.State, "newstate", "absent", "error", err)
c.State = "absent"
} else {
panic(err)
}
return nil, 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("Read() ", "type", c.Type(), "name", c.Name, "Id", c.Id)
return yaml.Marshal(c)
}
func (c *ContainerImage) Delete(ctx context.Context) error {
slog.Info("ContainerImage.Delete()", "image", c)
options := types.ImageRemoveOptions{
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
}