818 lines
23 KiB
Go
818 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) (c data.Resource) {
|
|
c = NewContainerImage(nil)
|
|
if u != nil {
|
|
if err := folio.CastParsedURI(u).ConstructResource(c); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
return
|
|
})
|
|
}
|
|
|
|
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) Init(u data.URIParser) (err error) {
|
|
if u == nil {
|
|
u = folio.URI(c.URI()).Parse()
|
|
}
|
|
err = c.SetParsedURI(u)
|
|
c.Name = ContainerImageNameFromURI(u.URL())
|
|
return
|
|
}
|
|
|
|
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) 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
|
|
}
|
|
|
|
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
|
|
}
|