293 lines
8.0 KiB
Go
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
|
|
}
|