jx/internal/resource/container_image.go
Matthew Rich 1117882ced
Some checks are pending
Lint / golangci-lint (push) Waiting to run
Declarative Tests / test (push) Waiting to run
update resources to common uri handling
2024-10-09 22:26:39 +00:00

832 lines
23 KiB
Go

// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
// Container resource
package resource
import (
"os"
"context"
"fmt"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
"log/slog"
"net/url"
"strings"
"encoding/json"
"encoding/base64"
"io"
"gitea.rosskeen.house/rosskeen.house/machine"
"decl/internal/codec"
"decl/internal/data"
"decl/internal/folio"
"decl/internal/transport"
"gopkg.in/yaml.v3"
"decl/internal/tempdir"
"archive/tar"
_ "strconv"
)
var ContextTempDir tempdir.Path = "jx_containerimage_context"
const (
ContainerImageTypeName TypeName = "container-image"
)
type ContainerProgressDetail struct {
ProgressMessage string `json:"message,omitempty" yaml:"message,omitempty"`
}
type ContainerLogStatus struct {
Status string `json:"status,omitempty" yaml:"status,omitempty"`
ProgressDetail ContainerProgressDetail `json:"progressDetail,omitempty" yaml:"progressDetail,omitempty"`
Id string `json:"id,omitempty" yaml:"id,omitempty"`
}
type ContainerLogStream struct {
Stream string `json:"stream,omitempty" yaml:"stream,omitempty"`
}
type ContainerLog struct {
*ContainerLogStream `json:",inline" yaml:",inline"`
*ContainerLogStatus `json:",inline" yaml:",inline"`
*ContainerError `json:",inline" yaml:",inline"`
}
type ContainerErrorDetail struct {
ErrorMessage string `json:"message" yaml:"message"`
}
type ContainerError struct {
Detail ContainerErrorDetail `json:"errorDetail" yaml:"errorDetail"`
Error string `json:"error" yaml:"error"`
}
type ContainerImageClient interface {
ImagePull(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error)
ImagePush(ctx context.Context, image string, options image.PushOptions) (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 {
*Common `yaml:",inline" json:",inline"`
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"`
DockerfileRef folio.ResourceReference `json:"dockerfileref,omitempty" yaml:"dockerfileref,omitempty"`
ContextRef folio.ResourceReference `json:"contextref,omitempty" yaml:"contextref,omitempty"`
InjectJX bool `json:"injectjx,omitempty" yaml:"injectjx,omitempty"`
PushImage bool `json:"push,omitempty" yaml:"push,omitempty"`
Output []ContainerLog `json:"output,omitempty" yaml:"output,omitempty"`
outputWriter strings.Builder `json:"-" yaml:"-"`
apiClient ContainerImageClient
Resources data.ResourceMapper `json:"-" yaml:"-"`
contextDocument data.Document `json:"-" yaml:"-"`
ConverterTypes data.TypesRegistry[data.Converter] `json:"-" yaml:"-"`
imageStat types.ImageInspect `json:"-" yaml:"-"`
}
func init() {
folio.DocumentRegistry.ResourceTypes.Register([]string{"container-image"}, func(u *url.URL) data.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)
}
}
c := &ContainerImage{
Common: NewCommon(ContainerImageTypeName, true),
apiClient: apiClient,
InjectJX: true,
PushImage: false,
ConverterTypes: folio.DocumentRegistry.ConverterTypes,
}
c.Common.NormalizePath = c.NormalizePath
return c
}
func (c *ContainerImage) RegistryAuthConfig() (authConfig registry.AuthConfig, err error) {
if c.Common.config != nil {
var configValue any
if configValue, err = c.Common.config.GetValue("repo_username"); err != nil {
return
} else {
authConfig.Username = configValue.(string)
}
if configValue, err = c.Common.config.GetValue("repo_password"); err != nil {
return
} else {
authConfig.Password = configValue.(string)
}
if configValue, err = c.Common.config.GetValue("repo_server"); err != nil {
return authConfig, nil
} else {
authConfig.ServerAddress = configValue.(string)
}
}
return
}
/*
func (c *ContainerImage) RegistryLogin(context context.Context) (token string, err error) {
var authConfig registry.AuthConfig
authConfig, err = c.RegistryAuthConfig()
if authResponse, loginErr := c.apiClient.RegistryLogin(context, authConfig); loginErr == nil {
return authResponse.IdentityToken, err
}
return
}
*/
func (c *ContainerImage) RegistryAuth() (string, error) {
if authConfig, err := c.RegistryAuthConfig(); err == nil {
if encodedJSON, jsonErr := json.Marshal(authConfig); jsonErr == nil {
slog.Info("ContainerImage.RegistryAuth()", "auth", authConfig, "encoded", encodedJSON, "error", jsonErr)
return base64.URLEncoding.EncodeToString(encodedJSON), nil
} else {
return "", jsonErr
}
} else {
slog.Info("ContainerImage.RegistryAuth()", "error", err)
return "", err
}
}
func (c *ContainerImage) NormalizePath() error {
return nil
}
func (c *ContainerImage) SetResourceMapper(resources data.ResourceMapper) {
c.Resources = resources
}
func (c *ContainerImage) Clone() data.Resource {
return &ContainerImage {
Common: c.Common,
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,
apiClient: c.apiClient,
contextDocument: c.contextDocument,
}
}
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("%s://%s/%s", ContainerImageTypeName, host, repo)
}
return fmt.Sprintf("%s://%s/%s", ContainerImageTypeName, 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 data.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_stat":
if statErr := c.ReadStat(ctx); statErr == nil {
if triggerErr := c.StateMachine().Trigger("exists"); triggerErr == nil {
return
}
} else {
if triggerErr := c.StateMachine().Trigger("notexists"); triggerErr == nil {
return
}
}
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 {
slog.Info("ContainerImage.Notify()", "created", c, "error", triggerErr)
return
} else {
slog.Info("ContainerImage.Notify()", "created", c, "error", triggerErr)
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(docData []byte, f codec.Format) (err error) {
err = f.StringDecoder(string(docData)).Decode(c)
return
}
func (c *ContainerImage) LoadReader(r io.ReadCloser, f codec.Format) (err error) {
err = f.Decoder(r).Decode(c)
return
}
func (c *ContainerImage) LoadString(docData string, f codec.Format) (err error) {
err = f.StringDecoder(docData).Decode(c)
return
}
func (c *ContainerImage) LoadDecl(yamlResourceDeclaration string) error {
return c.LoadString(yamlResourceDeclaration, codec.FormatYaml)
}
func (c *ContainerImage) SetContextDocument(document data.Document) {
c.contextDocument = document
}
func (c *ContainerImage) ContextDocument() (document data.Document, err error) {
var sourceRef data.Resource
if v, ok := folio.DocumentRegistry.GetDocument(folio.URI(c.ContextRef)); ok {
return v, nil
}
slog.Info("ContainerImage.ContextDocument()", "contextref", c.ContextRef, "resources", c.Resources)
if sourceRef = c.ContextRef.Dereference(c.Resources); sourceRef == nil {
if sourceRef, err = folio.URI(c.ContextRef).NewResource(nil); err != nil {
return
}
}
if sourceRef != nil {
slog.Info("ContainerImage.ContextDocument() - Dereference", "contextref", c.ContextRef, "ref", sourceRef, "uri", sourceRef.URI())
var extractor data.Converter
if extractor, err = c.ConverterTypes.New(sourceRef.URI()); err == nil {
if v, ok := extractor.(data.DirectoryConverter); ok {
v.SetRelative(true)
}
slog.Info("ContainerImage.ContextDocument() - Converter", "extractor", extractor, "sourceref", sourceRef.URI(), "type", extractor.Type())
if document, err = extractor.Extract(sourceRef, nil); err != nil {
return
}
}
} else {
err = ErrUnableToFindResource
return
}
if err = ContextTempDir.Create(); err != nil {
return
}
if ! ContextTempDir.ValidPath() {
err = fmt.Errorf("Invalid temp dir path: %s", ContextTempDir)
return
}
//defer ContextTempDir.Remove()
var dockerfileResource data.Resource
var tarDockerfile folio.URI = folio.URI("file://Dockerfile")
if dockerfileDecl, ok := document.Get(string(tarDockerfile)); ok {
dockerfileResource = dockerfileDecl.(data.Declaration).Resource()
} else {
if dockerfileResource, err = document.(*folio.Document).NewResourceFromURI(tarDockerfile); err != nil {
return
}
}
if len(c.Dockerfile) > 0 {
dockerfileResource.(data.FileResource).SetContentSourceRef("")
err = dockerfileResource.(data.FileResource).SetContent(strings.NewReader(c.Dockerfile))
slog.Info("ContainerImage.ContextDocument()", "dockerfile", dockerfileResource)
} else if len(c.DockerfileRef) > 0 {
dockerfileResource.(data.FileResource).SetContentSourceRef(string(c.DockerfileRef))
}
if c.InjectJX {
var jxResource data.Resource
var jxURI folio.URI
jxURI, err = JXPath()
slog.Info("ContainerImage.ContextDocument()", "jx", jxURI, "error", err)
if jxResource, err = document.(*folio.Document).NewResource("file://jx"); err != nil {
return
}
jxResource.(data.FileResource).SetContentSourceRef(string(jxURI))
slog.Info("ContainerImage.ContextDocument()", "jxResource", jxResource)
/*
fi, fiErr := data.FileInfoGetter(jxReader).Stat()
if fiErr != nil {
err = fiErr
return
}
jxResource.SetFileInfo(fi)
*/
}
return
}
// creates tmp context archive file from source context archive reader
func (c *ContainerImage) CreateContextArchive(reader io.ReadCloser) (contextTempFile folio.URI, err error) {
if err = ContextTempDir.Create(); err != nil {
return
}
if ! ContextTempDir.ValidPath() {
err = fmt.Errorf("Invalid temp dir path: %s", ContextTempDir)
return
}
//defer ContextTempDir.Remove()
contextTempFile = folio.URI(fmt.Sprintf("tar://%s/%s", ContextTempDir, "context.tar"))
writer, e := contextTempFile.ContentWriterStream()
if e != nil {
return contextTempFile, e
}
var header *tar.Header
tarReader := tar.NewReader(reader)
tarWriter := tar.NewWriter(writer)
defer tarWriter.Close()
for {
header, err = tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return
}
if err = tarWriter.WriteHeader(header); err != nil {
return
}
if _, err = io.Copy(tarWriter, tarReader); err != nil {
return
}
}
header = &tar.Header{
Name: "Dockerfile",
Mode: 0644,
}
if err = tarWriter.WriteHeader(header); err != nil {
return
}
var dockerfileReader io.Reader
if len(c.Dockerfile) > 0 {
dockerfileReader = strings.NewReader(c.Dockerfile)
} else {
if dockerfileReader, err = c.DockerfileRef.ContentReaderStream(); err != nil {
return
}
}
if _, err = io.Copy(tarWriter, dockerfileReader); err != nil {
return
}
if c.InjectJX {
var jxURI folio.URI
jxURI, err = JXPath()
var jxReader *transport.Reader
if jxReader, err = jxURI.ContentReaderStream(); err != nil {
return
}
fi, fiErr := data.FileInfoGetter(jxReader).Stat()
if fiErr != nil {
err = fiErr
return
}
slog.Info("ContainerImage.CreateContextArchive()", "jx", jxURI, "error", err)
header = &tar.Header{
Name: "jx",
Mode: 0755,
Size: fi.Size(),
}
if err = tarWriter.WriteHeader(header); err != nil {
return
}
if _, err = io.Copy(tarWriter, jxReader); err != nil {
return
}
}
return
}
func JXPath() (jxPath folio.URI, err error) {
var path string
path, err = os.Executable()
if err == nil {
jxPath = folio.URI(path)
if jxPath.Exists() {
return
} else {
err = os.ErrNotExist
}
}
jxPath = ""
return
}
func (c *ContainerImage) UnmarshalOutput() (err error) {
var containerErr ContainerError
var jsonBody string = c.outputWriter.String()
slog.Info("ContainerImage.UnmarshalOutput()", "json", jsonBody, "error", err)
for _, v := range strings.Split(jsonBody, "\r\n") {
var containerLog ContainerLog
outputDecoder := codec.NewJSONStringDecoder(v)
if decodeErr := outputDecoder.Decode(&containerLog); decodeErr != nil {
if decodeErr == io.EOF {
break
}
slog.Info("ContainerImage.UnmarshalOutput()", "value", v, "error", decodeErr)
err = decodeErr
}
if containerLog.ContainerLogStream != nil || containerLog.ContainerLogStatus != nil {
c.Output = append(c.Output, containerLog)
}
if containerLog.ContainerError != nil {
containerErr = *containerLog.ContainerError
c.Output = append(c.Output, containerLog)
}
slog.Info("ContainerImage.UnmarshalOutput()", "value", v, "error", err)
}
if len(containerErr.Error) > 0 {
return fmt.Errorf("%s", containerErr.Error)
}
return
}
// The contextref can be a tar file or a directory or maybe a loaded document
func (c *ContainerImage) Create(ctx context.Context) (err error) {
dockerfileURI := c.DockerfileRef.Parse()
buildOptions := types.ImageBuildOptions{
Dockerfile: dockerfileURI.Path,
Tags: []string{c.Name},
NetworkMode: "host",
}
var reader io.ReadCloser
if c.ContextRef.Exists() {
contentType := folio.URI(c.ContextRef).ContentType()
switch contentType {
case "tar", "tar.gz", "tgz":
var ctxArchiveURI folio.URI
r, refStreamErr := c.ContextRef.ContentReaderStream()
if refStreamErr != nil {
return refStreamErr
}
if ctxArchiveURI, err = c.CreateContextArchive(r); err != nil {
return err
}
reader, _ = ctxArchiveURI.ContentReaderStream()
default:
doc, ctErr := c.ContextDocument()
if ctErr != nil {
return ctErr
}
emitTar, tarErr := c.ConverterTypes.New(fmt.Sprintf("tar://%s/%s", ContextTempDir, "context.tar"))
if tarErr != nil {
return tarErr
}
slog.Info("ContainerImage.Create()", "document", doc, "error", err)
tarResource, emitErr := emitTar.Emit(doc, nil)
if emitErr != nil {
slog.Info("ContainerImage.Create() Emit", "document", doc, "error", emitErr)
return emitErr
}
emitTar.Close()
slog.Info("ContainerImage.Create()", "tar", tarResource, "error", err)
reader, _ = tarResource.(*File).GetContent(nil)
}
buildResponse, buildErr := c.apiClient.ImageBuild(ctx, reader, buildOptions)
slog.Info("ContainerImage.Create() - ImageBuild()", "buildResponse", buildResponse, "error", buildErr)
if buildErr != nil {
return buildErr
}
defer buildResponse.Body.Close()
copyBuffer := make([]byte, 32 * 1024)
if _, err = io.CopyBuffer(&c.outputWriter, buildResponse.Body, copyBuffer); err != nil {
slog.Info("ContainerImage.Create() - ImageBuild()", "error", err)
return fmt.Errorf("%w %s %s", err, c.Type(), c.Name)
} else {
if err = c.UnmarshalOutput(); err != nil {
return
}
/*
slog.Info("ContainerImage.Create() - ImageBuild()", "error", err)
var containerErr ContainerError
for _, jsonBody := range strings.Split(string(c.outputWriter.String()), "\r\n") {
decoder := codec.NewJSONStringDecoder(jsonBody)
decodeErr := decoder.Decode(&containerErr)
slog.Info("ContainerImage.Create() - ImageBuild()", "output", jsonBody, "error", containerErr, "decodeErr", decodeErr)
if len(containerErr.Error) > 0 {
return fmt.Errorf("%s", containerErr.Error)
}
}
*/
}
if c.PushImage {
err = c.Push(ctx)
slog.Info("ContainerImage.Create() - Push()", "error", err)
}
err = c.UnmarshalOutput()
}
return
}
func (c *ContainerImage) Update(ctx context.Context) error {
return c.Create(ctx)
}
func (c *ContainerImage) Push(ctx context.Context) (err error) {
var AuthToken string
AuthToken, err = c.RegistryAuth()
if err != nil {
return
}
/*
if err = c.apiClient.ImageTag(ctx, imageName, targetImage); err != nil {
return
}
*/
var response io.ReadCloser
if response, err = c.apiClient.ImagePush(context.Background(), c.Name, image.PushOptions{
RegistryAuth: AuthToken,
}); err != nil {
return
}
defer response.Close()
copyBuffer := make([]byte, 32 * 1024)
_, err = io.CopyBuffer(&c.outputWriter, response, copyBuffer)
//c.Output = c.outputWriter.String()
return
}
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) ReadStat(ctx context.Context) (err error) {
if c.imageStat, _, err = c.apiClient.ImageInspectWithRaw(ctx, c.Name); err != nil || c.imageStat.ID == "" {
return fmt.Errorf("%w: %s %s", err, c.Type(), c.Name)
}
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 {
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
}