Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: cache runx readme and config locally #14

Merged
merged 1 commit into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions internal/commands/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func NewCmd(dockerCli command.Cli, isPlugin bool) *cobra.Command {
src string
action string
lc = runkit.GetLocalConfig()
cache = runkit.NewLocalCache(dockerCli)
)

switch len(args) {
Expand Down Expand Up @@ -83,14 +84,14 @@ func NewCmd(dockerCli command.Cli, isPlugin bool) *cobra.Command {
Type(spinner.Globe).
Title(" Fetching runx details...").
Action(func() {
rk, err = runkit.Get(cmd.Context(), src)
rk, err = runkit.Get(cmd.Context(), cache, src)
if err != nil {
_, _ = fmt.Fprintln(dockerCli.Err(), err)
os.Exit(1)
}
}).Run()
} else {
rk, err = runkit.Get(cmd.Context(), src)
rk, err = runkit.Get(cmd.Context(), cache, src)
}
if err != nil {
return err
Expand Down
83 changes: 83 additions & 0 deletions runkit/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package runkit

import (
"os"
"path/filepath"

"github.com/docker/cli/cli/command"
"github.com/eunomie/docker-runx/internal/constants"
)

const (
runxConfigFile = "runx.yaml"
runxDocFile = "README.md"
)

var subCacheDir = filepath.Join(constants.SubCommandName, "cache", "sha256")

type LocalCache struct {
cacheDir string
}

func NewLocalCache(cli command.Cli) *LocalCache {
rootDir := filepath.Dir(cli.ConfigFile().Filename)
cacheDir := filepath.Join(rootDir, subCacheDir)

return &LocalCache{
cacheDir: cacheDir,
}
}

func (c *LocalCache) Get(digest string) (*RunKit, error) {
rk := &RunKit{
Files: make(map[string]string),
}
found := false

configFile := filepath.Join(c.cacheDir, digest, runxConfigFile)
if runxConfig, err := os.ReadFile(configFile); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
} else {
if err = decodeConfig(rk, digest, runxConfig); err != nil {
return nil, err
}
found = true
}

readmeFile := filepath.Join(c.cacheDir, digest, runxDocFile)
if runxDoc, err := os.ReadFile(readmeFile); err != nil {
if !os.IsNotExist(err) {
return nil, err
}
} else {
rk.Readme = string(runxDoc)
found = true
}

if found {
return rk, nil
}
return nil, nil
}

func (c *LocalCache) Set(digest string, runxConfig, runxDoc []byte) error {
digestDir := filepath.Join(c.cacheDir, digest)
if err := os.MkdirAll(digestDir, 0o755); err != nil {
return err
}
if len(runxConfig) > 0 {
configFile := filepath.Join(c.cacheDir, digest, runxConfigFile)
if err := os.WriteFile(configFile, runxConfig, 0o644); err != nil {
return err
}
}
if len(runxDoc) > 0 {
readmeFile := filepath.Join(c.cacheDir, digest, runxDocFile)
if err := os.WriteFile(readmeFile, runxDoc, 0o644); err != nil {
return err
}
}
return nil
}
154 changes: 94 additions & 60 deletions runkit/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,33 +16,40 @@ import (
"github.com/eunomie/docker-runx/internal/registry"
)

type Files struct {
Files []struct {
Name string `yaml:"name"`
Content string `yaml:"content"`
} `yaml:"files"`
}
type (
Files struct {
Files []struct {
Name string `yaml:"name"`
Content string `yaml:"content"`
} `yaml:"files"`
}

Cache interface {
Get(digest string) (*RunKit, error)
Set(digest string, runxConfig, runxDoc []byte) error
}
)

func Get(ctx context.Context, src string) (*RunKit, error) {
func Get(ctx context.Context, cache Cache, src string) (*RunKit, error) {
var (
err error
index v1.ImageIndex
desc *remote.Descriptor
manifest v1.Descriptor
runxImg v1.Image
layers []v1.Layer
runxConfig []byte
runxDoc []byte
files Files
config Config
rk = RunKit{
err error
desc *v1.Descriptor
indexDigest string
cached *RunKit
index v1.ImageIndex
manifest v1.Descriptor
runxImg v1.Image
layers []v1.Layer
runxConfig []byte
runxDoc []byte
rk = &RunKit{
Files: make(map[string]string),
}
remoteOpts = registry.WithOptions(ctx, nil)
ref, _ = name.ParseReference(src)
)

desc, err = remote.Get(ref, remoteOpts...)
desc, err = remote.Head(ref, remoteOpts...)
if err != nil {
return nil, fmt.Errorf("could not get image %s: %w", src, err)
}
Expand All @@ -51,6 +58,13 @@ func Get(ctx context.Context, src string) (*RunKit, error) {
return nil, fmt.Errorf("image %s can't be read by 'docker runx': should be an index", src)
}

indexDigest = desc.Digest.String()

cached, err = cache.Get(indexDigest)
if err == nil && cached != nil {
return cached, nil
}

index, err = remote.Index(ref, remoteOpts...)
if err != nil {
return nil, fmt.Errorf("could not get index %s: %w", src, err)
Expand Down Expand Up @@ -106,47 +120,10 @@ func Get(ctx context.Context, src string) (*RunKit, error) {
}

if len(runxConfig) != 0 {
dec := yaml.NewDecoder(bytes.NewReader(runxConfig))
dec.KnownFields(true)
// first, the runx config itself
if err = dec.Decode(&config); err != nil {
return nil, fmt.Errorf("could not decode runx config %s: %w", src, err)
}
// then, the optional files
if err = dec.Decode(&files); err != nil && err != io.EOF {
return nil, fmt.Errorf("could not decode runx files %s: %w", src, err)
} else {
for _, f := range files.Files {
c, err := b64Decode(f.Content)
if err != nil {
return nil, fmt.Errorf("could not decode runx file %s: %w", f.Name, err)
}
rk.Files[f.Name] = string(c)
}
}

if err = yaml.Unmarshal(runxConfig, &config); err != nil {
return nil, fmt.Errorf("could not unmarshal runx config %s: %w", src, err)
}
var actions []Action
for _, a := range config.Actions {
// TODO: fix reading of multiline YAML strings
a.Command = strings.ReplaceAll(a.Command, "\n", " ")

if a.Dockerfile != "" {
if c, ok := rk.Files[a.Dockerfile]; ok {
a.DockerfileContent = c
}
}

if config.Default == a.ID {
a.isDefault = true
}

actions = append(actions, a)
err = decodeConfig(rk, src, runxConfig)
if err != nil {
return nil, err
}
config.Actions = actions
rk.Config = config
}

if len(runxDoc) != 0 {
Expand All @@ -155,9 +132,66 @@ func Get(ctx context.Context, src string) (*RunKit, error) {

rk.src = src

return &rk, nil
err = cache.Set(indexDigest, runxConfig, runxDoc)
if err != nil {
// TODO: log error
return rk, nil
}

return rk, nil
}

func b64Decode(content string) ([]byte, error) {
return base64.StdEncoding.DecodeString(content)
}

func decodeConfig(rk *RunKit, src string, runxConfig []byte) error {
var (
config Config
err error
files Files
)
dec := yaml.NewDecoder(bytes.NewReader(runxConfig))
dec.KnownFields(true)
// first, the runx config itself
if err = dec.Decode(&config); err != nil {
return fmt.Errorf("could not decode runx config %s: %w", src, err)
}
// then, the optional files
if err = dec.Decode(&files); err != nil && err != io.EOF {
return fmt.Errorf("could not decode runx files %s: %w", src, err)
} else {
for _, f := range files.Files {
c, err := b64Decode(f.Content)
if err != nil {
return fmt.Errorf("could not decode runx file %s: %w", f.Name, err)
}
rk.Files[f.Name] = string(c)
}
}

if err = yaml.Unmarshal(runxConfig, &config); err != nil {
return fmt.Errorf("could not unmarshal runx config %s: %w", src, err)
}
var actions []Action
for _, a := range config.Actions {
// TODO: fix reading of multiline YAML strings
a.Command = strings.ReplaceAll(a.Command, "\n", " ")

if a.Dockerfile != "" {
if c, ok := rk.Files[a.Dockerfile]; ok {
a.DockerfileContent = c
}
}

if config.Default == a.ID {
a.isDefault = true
}

actions = append(actions, a)
}
config.Actions = actions
rk.Config = config

return nil
}
Loading