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: jfrog plugin + helm upload handler #1167

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion .github/workflows/components.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ permissions:
env:
REF: ${{ inputs.ref == '' && github.ref || inputs.ref }}
CTF_TYPE: directory
components: '["ocmcli", "helminstaller", "helmdemo", "subchartsdemo", "ecrplugin"]'
components: '["ocmcli", "helminstaller", "helmdemo", "subchartsdemo", "ecrplugin", "jfrogplugin"]'
IMAGE_PLATFORMS: 'linux/amd64 linux/arm64'
PLATFORMS: 'windows/amd64 darwin/arm64 darwin/amd64 linux/amd64 linux/arm64'

Expand Down
75 changes: 44 additions & 31 deletions api/ocm/plugin/cache/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,44 +296,57 @@ func (o *PluginUpdater) download(session ocm.Session, cv ocm.ComponentVersionAcc
return nil
}
}

dir := plugindirattr.Get(o.Context)
if dir != "" {
lock, err := filelock.LockDir(dir)
if dir == "" {
home, err := os.UserHomeDir() // use home if provided
if err != nil {
return err
return fmt.Errorf("failed to determine home directory to determine default plugin directory: %w", err)
}
defer lock.Close()

target := filepath.Join(dir, desc.PluginName)

verb := "installing"
if ok, _ := vfs.FileExists(fs, target); ok {
if !o.Force && (cv.GetVersion() == o.Current || !o.UpdateMode) {
return fmt.Errorf("plugin %s already found in %s", desc.PluginName, dir)
}
if o.UpdateMode {
verb = "updating"
}
fs.Remove(target)
dir = filepath.Join(home, plugindirattr.DEFAULT_PLUGIN_DIR)
if err := os.Mkdir(dir, os.ModePerm|os.ModeDir); err != nil {
return fmt.Errorf("failed to create default plugin directory: %w", err)
}
o.Printer.Printf("%s plugin %s[%s] in %s...\n", verb, desc.PluginName, desc.PluginVersion, dir)
dst, err := fs.OpenFile(target, vfs.O_CREATE|vfs.O_TRUNC|vfs.O_WRONLY, 0o755)
if err != nil {
return errors.Wrapf(err, "cannot create plugin file %s", target)
if err := plugindirattr.Set(o.Context, dir); err != nil {
return fmt.Errorf("failed to set plugin dir after defaulting: %w", err)
}
src, err := fs.OpenFile(file.Name(), vfs.O_RDONLY, 0)
if err != nil {
dst.Close()
return errors.Wrapf(err, "cannot open plugin executable %s", file.Name())
}

lock, err := filelock.LockDir(dir)
if err != nil {
return err
}
defer lock.Close()

target := filepath.Join(dir, desc.PluginName)

verb := "installing"
if ok, _ := vfs.FileExists(fs, target); ok {
if !o.Force && (cv.GetVersion() == o.Current || !o.UpdateMode) {
return fmt.Errorf("plugin %s already found in %s", desc.PluginName, dir)
Skarlso marked this conversation as resolved.
Show resolved Hide resolved
}
_, err = io.Copy(dst, src)
dst.Close()
utils.IgnoreError(src.Close())
utils.IgnoreError(os.Remove(file.Name()))
utils.IgnoreError(SetPluginSourceInfo(dir, cv, found.Meta().Name, desc.PluginName))
if err != nil {
return errors.Wrapf(err, "cannot copy plugin file %s", target)
if o.UpdateMode {
verb = "updating"
}
fs.Remove(target)
}
o.Printer.Printf("%s plugin %s[%s] in %s...\n", verb, desc.PluginName, desc.PluginVersion, dir)
dst, err := fs.OpenFile(target, vfs.O_CREATE|vfs.O_TRUNC|vfs.O_WRONLY, 0o755)
jakobmoellerdev marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return errors.Wrapf(err, "cannot create plugin file %s", target)
}
src, err := fs.OpenFile(file.Name(), vfs.O_RDONLY, 0)
if err != nil {
dst.Close()
return errors.Wrapf(err, "cannot open plugin executable %s", file.Name())
}
_, err = io.Copy(dst, src)
dst.Close()
utils.IgnoreError(src.Close())
utils.IgnoreError(os.Remove(file.Name()))
utils.IgnoreError(SetPluginSourceInfo(dir, cv, found.Meta().Name, desc.PluginName))
if err != nil {
return errors.Wrapf(err, "cannot copy plugin file %s", target)
}
}
return nil
Expand Down
31 changes: 27 additions & 4 deletions api/ocm/plugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import (
"fmt"
"io"
"os"
"strings"
"sync"

"github.com/mandelsoft/goutils/errors"
"github.com/mandelsoft/goutils/finalizer"
mlog "github.com/mandelsoft/logging"
"github.com/mandelsoft/vfs/pkg/vfs"

"ocm.software/ocm/api/credentials"
Expand Down Expand Up @@ -112,11 +114,32 @@ func (p *pluginImpl) Exec(r io.Reader, w io.Writer, args ...string) (result []by
args = append([]string{"--" + ppi.OptPlugingLogConfig, string(data)}, args...)
}

if len(p.config) == 0 {
p.ctx.Logger(TAG).Debug("execute plugin action", "path", p.Path(), "args", args)
} else {
p.ctx.Logger(TAG).Debug("execute plugin action", "path", p.Path(), "args", args, "config", p.config)
if p.ctx.Logger(TAG).Enabled(mlog.DebugLevel) {
Skarlso marked this conversation as resolved.
Show resolved Hide resolved
// Plainly kill any credentials found in the logger.
// Stupidly match for "credentials" arg.
// Not totally safe, but better than nothing.
logargs := make([]string, len(args))
for i, arg := range args {
if logargs[i] != "" {
continue
}
if strings.Contains(arg, "credentials") {
if strings.Contains(arg, "=") {
logargs[i] = "***"
} else if i < len(args)-1 {
jakobmoellerdev marked this conversation as resolved.
Show resolved Hide resolved
logargs[i+1] = "***"
}
}
logargs[i] = arg
}

if len(p.config) == 0 {
p.ctx.Logger(TAG).Debug("execute plugin action", "path", p.Path(), "args", logargs)
} else {
p.ctx.Logger(TAG).Debug("execute plugin action", "path", p.Path(), "args", logargs, "config", p.config)
}
}

data, err := cache.Exec(p.Path(), p.config, r, w, args...)

if logfile != nil {
Expand Down
24 changes: 13 additions & 11 deletions api/ocm/plugin/ppi/cmds/upload/put/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package put
import (
"encoding/json"
"fmt"
"io"
"os"

"github.com/mandelsoft/goutils/errors"
Expand Down Expand Up @@ -73,38 +72,41 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) {
func (o *Options) Complete(args []string) error {
o.Name = args[0]
if err := runtime.DefaultYAMLEncoding.Unmarshal([]byte(args[1]), &o.Specification); err != nil {
return errors.Wrapf(err, "invalid repository specification")
return fmt.Errorf("invalid repository specification: %w", err)
}
return nil
}

func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error {
spec, err := p.DecodeUploadTargetSpecification(opts.Specification)
if err != nil {
return errors.Wrapf(err, "target specification")
return fmt.Errorf("target specification: %w", err)
}

u := p.GetUploader(opts.Name)
if u == nil {
return errors.ErrNotFound(descriptor.KIND_UPLOADER, fmt.Sprintf("%s:%s", opts.ArtifactType, opts.MediaType))
}
w, h, err := u.Writer(p, opts.ArtifactType, opts.MediaType, opts.Hint, spec, opts.Credentials)

fi, err := os.Stdin.Stat()
if err != nil {
return err
fmt.Println("failed to stat stdin", err)
jakobmoellerdev marked this conversation as resolved.
Show resolved Hide resolved
}
_, err = io.Copy(w, os.Stdin)
if err != nil {
w.Close()
return err
if size := fi.Size(); size == 0 {
return fmt.Errorf("stdin is empty, and nothing can be uploaded")
}
err = w.Close()

h, err := u.Upload(p, opts.ArtifactType, opts.MediaType, opts.Hint, spec, opts.Credentials, os.Stdin)
if err != nil {
return err
return fmt.Errorf("upload failed: %w", err)
}

acc := h()

data, err := json.Marshal(acc)
if err == nil {
cmd.Printf("%s\n", string(data))
}

return err
}
2 changes: 1 addition & 1 deletion api/ocm/plugin/ppi/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ type Uploader interface {
Description() string

ValidateSpecification(p Plugin, spec UploadTargetSpec) (info *UploadTargetSpecInfo, err error)
Writer(p Plugin, arttype, mediatype string, hint string, spec UploadTargetSpec, creds credentials.Credentials) (io.WriteCloser, AccessSpecProvider, error)
Upload(p Plugin, arttype, mediatype string, hint string, spec UploadTargetSpec, creds credentials.Credentials, reader io.Reader) (AccessSpecProvider, error)
}

type UploadTargetSpec = runtime.TypedObject
Expand Down
19 changes: 12 additions & 7 deletions cmds/demoplugin/uploaders/demo.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (a *Uploader) Decoders() ppi.UploadFormats {
return types
}

func (a *Uploader) ValidateSpecification(p ppi.Plugin, spec ppi.UploadTargetSpec) (*ppi.UploadTargetSpecInfo, error) {
func (a *Uploader) ValidateSpecification(_ ppi.Plugin, spec ppi.UploadTargetSpec) (*ppi.UploadTargetSpecInfo, error) {
var info ppi.UploadTargetSpecInfo
my := spec.(*TargetSpec)

Expand All @@ -72,21 +72,21 @@ func (a *Uploader) ValidateSpecification(p ppi.Plugin, spec ppi.UploadTargetSpec
return &info, nil
}

func (a *Uploader) Writer(p ppi.Plugin, arttype, mediatype, hint string, repo ppi.UploadTargetSpec, creds credentials.Credentials) (io.WriteCloser, ppi.AccessSpecProvider, error) {
func (a *Uploader) Upload(p ppi.Plugin, _, mediatype, hint string, repo ppi.UploadTargetSpec, _ credentials.Credentials, reader io.Reader) (ppi.AccessSpecProvider, error) {
var file *os.File
var err error

cfg, err := p.GetConfig()
if err != nil {
return nil, nil, errors.Wrapf(err, "can't get config for access method %s", mediatype)
return nil, errors.Wrapf(err, "can't get config for access method %s", mediatype)
}

root := os.TempDir()
if cfg != nil && cfg.(*config.Config).Uploaders.Path != "" {
root = cfg.(*config.Config).Uploaders.Path
err := os.MkdirAll(root, 0o700)
if err != nil {
return nil, nil, errors.Wrapf(err, "cannot create root dir")
return nil, errors.Wrapf(err, "cannot create root dir")
}
}

Expand All @@ -106,7 +106,7 @@ func (a *Uploader) Writer(p ppi.Plugin, arttype, mediatype, hint string, repo pp

err = os.MkdirAll(dir, 0o700)
if err != nil {
return nil, nil, err
return nil, err
}

if hint == "" {
Expand All @@ -115,8 +115,13 @@ func (a *Uploader) Writer(p ppi.Plugin, arttype, mediatype, hint string, repo pp
file, err = os.OpenFile(filepath.Join(os.TempDir(), path), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600)
}
if err != nil {
return nil, nil, err
return nil, err
}
writer := NewWriter(file, path, mediatype, hint == "", accessmethods.NAME, accessmethods.VERSION)
return writer, writer.Specification, nil

if _, err = io.Copy(writer, reader); err != nil {
return nil, fmt.Errorf("cannot write to %q: %w", file.Name(), err)
}

return writer.Specification, nil
}
69 changes: 69 additions & 0 deletions cmds/jfrogplugin/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package main

import (
"encoding/json"
"fmt"
"os"
"strconv"

"ocm.software/ocm/api/config"
"ocm.software/ocm/api/ocm/extensions/artifacttypes"
"ocm.software/ocm/api/ocm/extensions/blobhandler"
"ocm.software/ocm/api/ocm/plugin"
"ocm.software/ocm/api/ocm/plugin/ppi"
"ocm.software/ocm/api/ocm/plugin/ppi/cmds"
"ocm.software/ocm/api/version"
"ocm.software/ocm/cmds/jfrogplugin/uploaders/helm"
)

const NAME = "jfrog"

func main() {
p := ppi.NewPlugin(NAME, version.Get().String())

p.SetShort(NAME + " plugin")
p.SetLong(`ALPHA GRADE plugin providing custom functions related to interacting with JFrog Repositories (e.g. Artifactory).

This plugin is solely for interacting with JFrog Servers and should not be used for generic repository interactions.
Skarlso marked this conversation as resolved.
Show resolved Hide resolved
Thus, you should only consider this plugin if
- You need to use a JFrog specific API
- You cannot use any of the generic (non-jfrog) implementations.

Examples:

You can configure the JFrog plugin as an Uploader in an ocm config file with:

- type: ` + fmt.Sprintf("%s.ocm.%s", plugin.KIND_UPLOADER, config.OCM_CONFIG_TYPE_SUFFIX) + `
registrations:
- name: ` + fmt.Sprintf("%s/%s/%s", plugin.KIND_PLUGIN, NAME, helm.NAME) + `
artifactType: ` + artifacttypes.HELM_CHART + `
priority: 200 # must be > ` + strconv.Itoa(blobhandler.DEFAULT_BLOBHANDLER_PRIO) + ` to be used over the default handler
config:
type: ` + fmt.Sprintf("%s/%s", helm.NAME, helm.VERSION) + `
# this is only a sample JFrog Server URL, do NOT append /artifactory
url: int.repositories.ocm.software
repository: ocm-helm-test
`)
p.SetConfigParser(GetConfig)

u := helm.New()
if err := p.RegisterUploader(artifacttypes.HELM_CHART, "", u); err != nil {
panic(err)
}
err := cmds.NewPluginCommand(p).Execute(os.Args[1:])
if err != nil {
jakobmoellerdev marked this conversation as resolved.
Show resolved Hide resolved
os.Exit(1)
}
}

type Config struct {
}

func GetConfig(raw json.RawMessage) (interface{}, error) {
var cfg Config

if err := json.Unmarshal(raw, &cfg); err != nil {
return nil, fmt.Errorf("could not get config: %w", err)
}
return &cfg, nil
}
25 changes: 25 additions & 0 deletions cmds/jfrogplugin/uploaders/helm/headers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package helm

import (
"net/http"

"ocm.software/ocm/api/credentials"
)

func SetHeadersFromCredentials(req *http.Request, creds credentials.Credentials) {
if creds == nil {
return
}
if creds.ExistsProperty(credentials.ATTR_TOKEN) {
req.Header.Set("Authorization", "Bearer "+creds.GetProperty(credentials.ATTR_TOKEN))
} else {
var user, pass string
if creds.ExistsProperty(credentials.ATTR_USERNAME) {
user = creds.GetProperty(credentials.ATTR_USERNAME)
}
if creds.ExistsProperty(credentials.ATTR_PASSWORD) {
pass = creds.GetProperty(credentials.ATTR_PASSWORD)
}
req.SetBasicAuth(user, pass)
}
}
Loading
Loading