296 lines
7.0 KiB
Go
296 lines
7.0 KiB
Go
|
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
|
||
|
|
||
|
package resource
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"fmt"
|
||
|
"github.com/docker/docker/api/types/filters"
|
||
|
"github.com/docker/docker/api/types/volume"
|
||
|
_ "github.com/docker/docker/api/types/strslice"
|
||
|
"github.com/docker/docker/client"
|
||
|
"gopkg.in/yaml.v3"
|
||
|
_ "log/slog"
|
||
|
"net/url"
|
||
|
_ "os"
|
||
|
_ "os/exec"
|
||
|
_ "strings"
|
||
|
"encoding/json"
|
||
|
"io"
|
||
|
"gitea.rosskeen.house/rosskeen.house/machine"
|
||
|
"decl/internal/codec"
|
||
|
"decl/internal/data"
|
||
|
"decl/internal/folio"
|
||
|
"log/slog"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
ContainerVolumeTypeName TypeName = "container-volume"
|
||
|
)
|
||
|
|
||
|
type ContainerVolumeClient interface {
|
||
|
ContainerClient
|
||
|
VolumeCreate(ctx context.Context, options volume.CreateOptions) (volume.Volume, error)
|
||
|
VolumeList(ctx context.Context, options volume.ListOptions) (volume.ListResponse, error)
|
||
|
VolumeInspect(ctx context.Context, volumeID string) (volume.Volume, error)
|
||
|
VolumeRemove(ctx context.Context, volumeID string, force bool) (error)
|
||
|
}
|
||
|
|
||
|
type ContainerVolume struct {
|
||
|
*Common `json:",inline" yaml:",inline"`
|
||
|
stater machine.Stater `json:"-" yaml:"-"`
|
||
|
volume.Volume `json:",inline" yaml:",inline"`
|
||
|
|
||
|
apiClient ContainerVolumeClient
|
||
|
Resources data.ResourceMapper `json:"-" yaml:"-"`
|
||
|
}
|
||
|
|
||
|
func init() {
|
||
|
folio.DocumentRegistry.ResourceTypes.Register([]string{"container-volume"}, func(u *url.URL) (n data.Resource) {
|
||
|
n = NewContainerVolume(nil)
|
||
|
if u != nil {
|
||
|
if err := folio.CastParsedURI(u).ConstructResource(n); err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
}
|
||
|
return
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func NewContainerVolume(containerClientApi ContainerVolumeClient) (cn *ContainerVolume) {
|
||
|
var apiClient ContainerVolumeClient = containerClientApi
|
||
|
if apiClient == nil {
|
||
|
var err error
|
||
|
apiClient, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
}
|
||
|
cn = &ContainerVolume{
|
||
|
apiClient: apiClient,
|
||
|
}
|
||
|
cn.Common = NewCommon(ContainerVolumeTypeName, true)
|
||
|
cn.Common.NormalizePath = cn.NormalizePath
|
||
|
return cn
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) Init(u data.URIParser) error {
|
||
|
if u == nil {
|
||
|
u = folio.URI(v.URI()).Parse()
|
||
|
}
|
||
|
return v.SetParsedURI(u)
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) NormalizePath() error {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) SetResourceMapper(resources data.ResourceMapper) {
|
||
|
v.Resources = resources
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) Clone() data.Resource {
|
||
|
return &ContainerVolume {
|
||
|
Common: v.Common.Clone(),
|
||
|
Volume: v.Volume,
|
||
|
apiClient: v.apiClient,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) StateMachine() machine.Stater {
|
||
|
if v.stater == nil {
|
||
|
v.stater = StorageMachine(v)
|
||
|
}
|
||
|
return v.stater
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) Notify(m *machine.EventMessage) {
|
||
|
ctx := context.Background()
|
||
|
slog.Info("Notify()", "ContainerVolume", v, "m", m)
|
||
|
switch m.On {
|
||
|
case machine.ENTERSTATEEVENT:
|
||
|
switch m.Dest {
|
||
|
case "start_read":
|
||
|
if _,readErr := v.Read(ctx); readErr == nil {
|
||
|
if triggerErr := v.StateMachine().Trigger("state_read"); triggerErr == nil {
|
||
|
return
|
||
|
} else {
|
||
|
v.Common.State = "absent"
|
||
|
panic(triggerErr)
|
||
|
}
|
||
|
} else {
|
||
|
v.Common.State = "absent"
|
||
|
panic(readErr)
|
||
|
}
|
||
|
case "start_delete":
|
||
|
if deleteErr := v.Delete(ctx); deleteErr == nil {
|
||
|
if triggerErr := v.StateMachine().Trigger("deleted"); triggerErr == nil {
|
||
|
return
|
||
|
} else {
|
||
|
v.Common.State = "present"
|
||
|
panic(triggerErr)
|
||
|
}
|
||
|
} else {
|
||
|
v.Common.State = "present"
|
||
|
panic(deleteErr)
|
||
|
}
|
||
|
case "start_create":
|
||
|
if e := v.Create(ctx); e == nil {
|
||
|
if triggerErr := v.StateMachine().Trigger("created"); triggerErr == nil {
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
v.Common.State = "absent"
|
||
|
case "absent":
|
||
|
v.Common.State = "absent"
|
||
|
case "present", "created", "read":
|
||
|
v.Common.State = "present"
|
||
|
}
|
||
|
case machine.EXITSTATEEVENT:
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) URI() string {
|
||
|
return fmt.Sprintf("container-volume://%s", v.Name)
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) JSON() ([]byte, error) {
|
||
|
return json.Marshal(v)
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) Validate() error {
|
||
|
return fmt.Errorf("failed")
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) Apply() error {
|
||
|
ctx := context.Background()
|
||
|
switch v.Common.State {
|
||
|
case "absent":
|
||
|
return v.Delete(ctx)
|
||
|
case "present":
|
||
|
return v.Create(ctx)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) Load(docData []byte, f codec.Format) (err error) {
|
||
|
err = f.StringDecoder(string(docData)).Decode(v)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) LoadReader(r io.ReadCloser, f codec.Format) (err error) {
|
||
|
err = f.Decoder(r).Decode(v)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) LoadString(docData string, f codec.Format) (err error) {
|
||
|
err = f.StringDecoder(docData).Decode(v)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
func (n *ContainerVolume) LoadDecl(yamlResourceDeclaration string) error {
|
||
|
return n.LoadString(yamlResourceDeclaration, codec.FormatYaml)
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) Create(ctx context.Context) (err error) {
|
||
|
var spec volume.ClusterVolumeSpec
|
||
|
if v.ClusterVolume != nil {
|
||
|
spec = v.ClusterVolume.Spec
|
||
|
}
|
||
|
v.Volume, err = v.apiClient.VolumeCreate(ctx, volume.CreateOptions{
|
||
|
Name: v.Name,
|
||
|
Driver: v.Driver,
|
||
|
DriverOpts: v.Options,
|
||
|
Labels: v.Labels,
|
||
|
ClusterVolumeSpec: &spec,
|
||
|
})
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) Inspect(ctx context.Context, volumeID string) error {
|
||
|
volumeInspect, err := v.apiClient.VolumeInspect(ctx, volumeID)
|
||
|
if client.IsErrNotFound(err) {
|
||
|
v.Common.State = "absent"
|
||
|
} else {
|
||
|
v.Common.State = "present"
|
||
|
v.Volume = volumeInspect
|
||
|
if v.Name == "" {
|
||
|
if volumeInspect.Name[0] == '/' {
|
||
|
v.Name = volumeInspect.Name[1:]
|
||
|
} else {
|
||
|
v.Name = volumeInspect.Name
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) Read(ctx context.Context) ([]byte, error) {
|
||
|
var volumeID string
|
||
|
filterArgs := filters.NewArgs()
|
||
|
filterArgs.Add("name", v.Name)
|
||
|
volumes, err := v.apiClient.VolumeList(ctx, volume.ListOptions{
|
||
|
Filters: filterArgs,
|
||
|
})
|
||
|
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("%w: %s %s", err, v.Type(), v.Name)
|
||
|
}
|
||
|
|
||
|
for _, vol := range volumes.Volumes {
|
||
|
if vol.Name == v.Name {
|
||
|
volumeID = vol.Name
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if inspectErr := v.Inspect(ctx, volumeID); inspectErr != nil {
|
||
|
return nil, fmt.Errorf("%w: volume %s", inspectErr, volumeID)
|
||
|
}
|
||
|
slog.Info("Read() ", "type", v.Type(), "name", v.Name)
|
||
|
|
||
|
return yaml.Marshal(v)
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) Update(ctx context.Context) error {
|
||
|
return v.Create(ctx)
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) Delete(ctx context.Context) error {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (v *ContainerVolume) Type() string { return "container-volume" }
|
||
|
|
||
|
func (v *ContainerVolume) ResolveId(ctx context.Context) string {
|
||
|
v.Inspect(ctx, v.Name)
|
||
|
return v.Name
|
||
|
/*
|
||
|
volumes, err := n.apiClient.VolumeInspect(ctx, volume.ListOptions{
|
||
|
|
||
|
filterArgs := filters.NewArgs()
|
||
|
filterArgs.Add("name", "/"+n.Name)
|
||
|
volumes, err := n.apiClient.VolumeList(ctx, volume.ListOptions{
|
||
|
All: true,
|
||
|
Filters: filterArgs,
|
||
|
})
|
||
|
if err != nil {
|
||
|
panic(fmt.Errorf("%w: %s %s", err, n.Type(), n.Name))
|
||
|
}
|
||
|
|
||
|
for _, volume := range volumes {
|
||
|
for _, containerName := range volume.Name {
|
||
|
if containerName == n.Name {
|
||
|
if n.Id == "" {
|
||
|
n.Id = container.ID
|
||
|
}
|
||
|
return container.ID
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return ""
|
||
|
*/
|
||
|
}
|