jx/internal/resource/container_image.go

395 lines
11 KiB
Go
Raw Normal View History

2024-05-24 05:11:51 +00:00
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
// Container resource
package resource
import (
"context"
"fmt"
"github.com/docker/docker/api/types"
2024-05-26 07:15:50 +00:00
"github.com/docker/docker/api/types/image"
2024-05-24 05:11:51 +00:00
"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 {
2024-07-17 08:34:57 +00:00
ImagePull(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error)
2024-05-24 05:11:51 +00:00
ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error)
2024-07-17 08:34:57 +00:00
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)
2024-05-24 05:11:51 +00:00
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"`
2024-05-24 05:11:51 +00:00
config ConfigurationValueGetter
2024-05-24 05:11:51 +00:00
apiClient ContainerImageClient
2024-07-17 08:34:57 +00:00
Resources ResourceMapper `json:"-" yaml:"-"`
2024-05-24 05:11:51 +00:00
}
func init() {
ResourceTypes.Register([]string{"container-image"}, func(u *url.URL) Resource {
2024-05-24 05:11:51 +00:00
c := NewContainerImage(nil)
2024-07-17 08:34:57 +00:00
c.Name = ContainerImageNameFromURI(u)
slog.Info("NewContainerImage", "container", c)
2024-05-24 05:11:51 +00:00
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,
2024-05-24 05:11:51 +00:00
}
}
2024-07-17 08:34:57 +00:00
func (c *ContainerImage) SetResourceMapper(resources ResourceMapper) {
c.Resources = resources
}
2024-05-24 05:11:51 +00:00
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,
2024-05-24 05:11:51 +00:00
State: c.State,
apiClient: c.apiClient,
}
}
func (c *ContainerImage) StateMachine() machine.Stater {
if c.stater == nil {
c.stater = StorageMachine(c)
}
return c.stater
}
2024-07-17 08:34:57 +00:00
func URIFromContainerImageName(imageName string) string {
2024-05-24 05:11:51 +00:00
var host, namespace, repo string
2024-07-17 08:34:57 +00:00
elements := strings.Split(imageName, "/")
2024-05-24 05:11:51 +00:00
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}, "/"))
}
2024-07-17 08:34:57 +00:00
// 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)
}
2024-05-24 05:11:51 +00:00
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
}
2024-05-24 05:11:51 +00:00
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)
}
2024-05-24 05:11:51 +00:00
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)
}
}
}
2024-05-24 05:11:51 +00:00
return nil
}
func (c *ContainerImage) Update(ctx context.Context) error {
return c.Create(ctx)
}
2024-07-17 08:34:57 +00:00
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
}
2024-05-24 05:11:51 +00:00
2024-07-17 08:34:57 +00:00
func (c *ContainerImage) Inspect(ctx context.Context) (imageInspect types.ImageInspect) {
var err error
imageInspect, _, err = c.apiClient.ImageInspectWithRaw(ctx, c.Name)
2024-05-24 05:11:51 +00:00
if err != nil {
2024-07-17 08:34:57 +00:00
panic(err)
2024-05-24 05:11:51 +00:00
}
2024-07-17 08:34:57 +00:00
return
}
2024-05-24 05:11:51 +00:00
2024-07-17 08:34:57 +00:00
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))
}
}()
2024-05-24 05:11:51 +00:00
2024-07-17 08:34:57 +00:00
var imageInspect types.ImageInspect
imageInspect, _, err = c.apiClient.ImageInspectWithRaw(ctx, c.Name)
slog.Info("ContainerImage.Read()", "name", c.Name, "error", err)
2024-05-24 05:11:51 +00:00
if err != nil {
if client.IsErrNotFound(err) {
2024-07-17 08:34:57 +00:00
if pullErr := c.Pull(ctx); pullErr != nil {
panic(pullErr)
}
imageInspect = c.Inspect(ctx)
2024-05-24 05:11:51 +00:00
} 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
2024-07-17 08:34:57 +00:00
slog.Info("ContainerImage.Read()", "type", c.Type(), "name", c.Name, "Id", c.Id, "state", c.State, "error", err)
2024-05-24 05:11:51 +00:00
return yaml.Marshal(c)
}
func (c *ContainerImage) Delete(ctx context.Context) error {
slog.Info("ContainerImage.Delete()", "image", c)
2024-07-17 08:34:57 +00:00
options := image.RemoveOptions{
2024-05-24 05:11:51 +00:00
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 != "" {
2024-05-26 09:14:16 +00:00
if triggerErr := c.StateMachine().Trigger("exists"); triggerErr != nil {
panic(fmt.Errorf("%w: %s %s", triggerErr, c.Type(), c.Name))
}
2024-05-24 05:11:51 +00:00
slog.Info("ContainerImage.ResolveId() trigger created", "machine", c.StateMachine(), "state", c.StateMachine().CurrentState())
} else {
2024-05-26 09:14:16 +00:00
if triggerErr := c.StateMachine().Trigger("notexists"); triggerErr != nil {
panic(fmt.Errorf("%w: %s %s", triggerErr, c.Type(), c.Name))
}
2024-05-24 05:11:51 +00:00
slog.Info("ContainerImage.ResolveId()", "name", c.Name, "machine.state", c.StateMachine().CurrentState(), "resource.state", c.State)
}
return c.Id
}