diff --git a/.github/workflows/components.yaml b/.github/workflows/components.yaml
index c519473c20..16c18e956e 100644
--- a/.github/workflows/components.yaml
+++ b/.github/workflows/components.yaml
@@ -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'
diff --git a/api/ocm/extensions/blobhandler/handlers/generic/http/blobhandler.go b/api/ocm/extensions/blobhandler/handlers/generic/http/blobhandler.go
deleted file mode 100644
index 046eb65ead..0000000000
--- a/api/ocm/extensions/blobhandler/handlers/generic/http/blobhandler.go
+++ /dev/null
@@ -1,105 +0,0 @@
-package maven
-
-import (
- "bytes"
- "context"
- "errors"
- "fmt"
- "net/http"
- "net/url"
- "text/template"
-
- "github.com/google/go-containerregistry/pkg/v1/remote"
- mlog "github.com/mandelsoft/logging"
-
- "ocm.software/ocm/api/ocm/cpi"
- "ocm.software/ocm/api/ocm/extensions/accessmethods/helm"
- resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
- "ocm.software/ocm/api/ocm/extensions/blobhandler/handlers/generic/http/identity"
- "ocm.software/ocm/api/utils"
- "ocm.software/ocm/api/utils/logging"
-)
-
-const REALM = "http"
-
-const BlobHandlerName = "ocm/" + "http"
-
-type artifactHandler struct {
- spec *Config
-}
-
-func NewArtifactHandler(repospec *Config) cpi.BlobHandler {
- return &artifactHandler{spec: repospec}
-}
-
-// blob => http.Request => http.Response => cpi.AccessSpec (HelmAccess, WgetAccess, etc.)
-func (b *artifactHandler) StoreBlob(blob cpi.BlobAccess, resourceType string, hint string, _ cpi.AccessSpec, ctx cpi.StorageContext) (_ cpi.AccessSpec, rerr error) {
- remote, err := b.URL(ctx, blob)
- if err != nil {
- return nil, fmt.Errorf("failed to get remote for blob: %w", err)
- }
- data, err := blob.Reader()
- if err != nil {
- return nil, fmt.Errorf("failed to read blob: %w", err)
- }
- defer func() {
- rerr = errors.Join(rerr, data.Close())
- }()
-
- rawURL := remote.String()
-
- req, err := http.NewRequestWithContext(context.TODO(), b.spec.Method, rawURL, data)
- if err != nil {
- return nil, fmt.Errorf("failed to create request: %w", err)
- }
-
- creds := identity.GetCredentials(ctx.GetContext(), rawURL)
- user, pass := creds[identity.ATTR_USERNAME], creds[identity.ATTR_PASSWORD]
- if user != "" && pass != "" {
- req.SetBasicAuth(creds["username"], creds["password"])
- }
-
- client := Client(b.spec)
- client.Transport = logging.NewRoundTripper(client.Transport, logging.DynamicLogger(ctx, REALM,
- mlog.NewAttribute(logging.ATTR_HOST, remote.Host),
- mlog.NewAttribute(logging.ATTR_PATH, remote.Path),
- mlog.NewAttribute(logging.ATTR_USER, user),
- ))
-
- response, err := client.Do(req)
- if err != nil {
- return nil, fmt.Errorf("failed to store blob via request: %w", err)
- }
-
- //TODO: replace with registry based mapping so we dont have to bind the api here
- switch resourceType {
- case resourcetypes.HELM_CHART:
- return helm.New("demoapp", "https://int.repositories.cloud.sap/artifactory/api/helm/ocm-helm-test"), nil
- }
-
- return nil, errors.New("not implemented")
-}
-
-func (h *artifactHandler) URL(ctx cpi.StorageContext, blob cpi.BlobAccess) (*url.URL, error) {
- tpl, err := template.New("").Parse(h.spec.RepositoryURL)
- if err != nil {
- return nil, fmt.Errorf("failed to parse url template: %w", err)
- }
- var buf bytes.Buffer
- err = tpl.Execute(&buf, map[string]string{
- "mimeType": blob.MimeType(),
- "size": fmt.Sprintf("%d", blob.Size()),
- "digest": string(blob.Digest()),
- "component": ctx.TargetComponentName(),
- })
- if err != nil {
- return nil, fmt.Errorf("failed to execute url template: %w", err)
- }
- return utils.ParseURL(buf.String())
-}
-
-func Client(_ *Config) *http.Client {
- return &http.Client{
- Transport: remote.DefaultTransport,
- }
-}
diff --git a/api/ocm/extensions/blobhandler/handlers/generic/http/identity/identity.go b/api/ocm/extensions/blobhandler/handlers/generic/http/identity/identity.go
deleted file mode 100644
index 9504a1009b..0000000000
--- a/api/ocm/extensions/blobhandler/handlers/generic/http/identity/identity.go
+++ /dev/null
@@ -1,74 +0,0 @@
-package identity
-
-import (
- "ocm.software/ocm/api/credentials/cpi"
- "ocm.software/ocm/api/credentials/identity/hostpath"
- "ocm.software/ocm/api/utils/listformat"
- common "ocm.software/ocm/api/utils/misc"
-)
-
-// CONSUMER_TYPE is the Helm chart repository type.
-const CONSUMER_TYPE = "HTTPUploader"
-
-// ID_TYPE is the type field of a consumer identity.
-const ID_TYPE = cpi.ID_TYPE
-
-// ID_SCHEME is the scheme of the repository.
-const ID_SCHEME = hostpath.ID_SCHEME
-
-// ID_HOSTNAME is the hostname of a repository.
-const ID_HOSTNAME = hostpath.ID_HOSTNAME
-
-// ID_PORT is the port number of a repository.
-const ID_PORT = hostpath.ID_PORT
-
-// ID_PATHPREFIX is the path of a repository.
-const ID_PATHPREFIX = hostpath.ID_PATHPREFIX
-
-func init() {
- attrs := listformat.FormatListElements("", listformat.StringElementDescriptionList{
- ATTR_USERNAME, "the basic auth user name",
- ATTR_PASSWORD, "the basic auth password",
- ATTR_CERTIFICATE, "TLS client certificate",
- ATTR_PRIVATE_KEY, "TLS private key",
- ATTR_CERTIFICATE_AUTHORITY, "TLS certificate authority",
- })
-
- cpi.RegisterStandardIdentity(CONSUMER_TYPE, IdentityMatcher, `HTTPUploader
-
-It matches the `+CONSUMER_TYPE+`
consumer type and additionally acts like
-the `+hostpath.IDENTITY_TYPE+`
type.`,
- attrs)
-}
-
-var identityMatcher = hostpath.IdentityMatcher("")
-
-func IdentityMatcher(pattern, cur, id cpi.ConsumerIdentity) bool {
- return identityMatcher(pattern, cur, id)
-}
-
-// used credential attributes
-
-const (
- ATTR_USERNAME = cpi.ATTR_USERNAME
- ATTR_PASSWORD = cpi.ATTR_PASSWORD
- ATTR_CERTIFICATE_AUTHORITY = cpi.ATTR_CERTIFICATE_AUTHORITY
- ATTR_CERTIFICATE = cpi.ATTR_CERTIFICATE
- ATTR_PRIVATE_KEY = cpi.ATTR_PRIVATE_KEY
- ATTR_TOKEN = cpi.ATTR_TOKEN
-)
-
-func GetCredentials(ctx cpi.ContextProvider, consumerType string, url string) common.Properties {
- if consumerType == "" {
- consumerType = CONSUMER_TYPE
- }
- id := hostpath.GetConsumerIdentity(consumerType, url)
- if id == nil {
- return nil
- }
- creds, err := cpi.CredentialsForConsumer(ctx.CredentialsContext(), id)
- if creds == nil || err != nil {
- return nil
- }
- return creds.Properties()
-}
diff --git a/api/ocm/extensions/blobhandler/handlers/generic/http/registration.go b/api/ocm/extensions/blobhandler/handlers/generic/http/registration.go
deleted file mode 100644
index ec69d82554..0000000000
--- a/api/ocm/extensions/blobhandler/handlers/generic/http/registration.go
+++ /dev/null
@@ -1,84 +0,0 @@
-package maven
-
-import (
- "encoding/json"
- "fmt"
-
- "github.com/mandelsoft/goutils/errors"
-
- "ocm.software/ocm/api/ocm/cpi"
- resourcetypes "ocm.software/ocm/api/ocm/extensions/artifacttypes"
- "ocm.software/ocm/api/utils/mime"
- "ocm.software/ocm/api/utils/registrations"
-)
-
-func init() {
- cpi.RegisterBlobHandlerRegistrationHandler(BlobHandlerName, &RegistrationHandler{})
-}
-
-type Config struct {
- RepositoryURL string `json:"url"`
- Path string `json:"path"`
- Method string `json:"method"`
- Credentials CredentialsConfig `json:"credentials"`
-}
-
-type CredentialsConfig struct {
- Method string `json:"method"`
-}
-
-type rawConfig Config
-
-func (c *Config) UnmarshalJSON(data []byte) error {
- err := json.Unmarshal(data, &c.RepositoryURL)
- if err == nil {
- return nil
- }
- var raw rawConfig
- err = json.Unmarshal(data, &raw)
- if err != nil {
- return err
- }
- *c = Config(raw)
-
- return nil
-}
-
-type RegistrationHandler struct{}
-
-var _ cpi.BlobHandlerRegistrationHandler = (*RegistrationHandler)(nil)
-
-func (r *RegistrationHandler) RegisterByName(handler string, ctx cpi.Context, config cpi.BlobHandlerConfig, olist ...cpi.BlobHandlerOption) (bool, error) {
- if handler != "" {
- return true, fmt.Errorf("invalid %s handler %q", resourcetypes.HELM_CHART, handler)
- }
- if config == nil {
- return true, fmt.Errorf("http repository specification required")
- }
- cfg, err := registrations.DecodeConfig[Config](config)
- if err != nil {
- return true, errors.Wrapf(err, "blob handler configuration")
- }
-
- ctx.BlobHandlers().Register(NewArtifactHandler(cfg),
- cpi.ForArtifactType(resourcetypes.HELM_CHART),
- cpi.ForArtifactType(resourcetypes.BLOB),
- cpi.ForMimeType(mime.MIME_TGZ),
- cpi.ForMimeType(mime.MIME_TGZ_ALT),
- cpi.NewBlobHandlerOptions(olist...),
- )
-
- return true, nil
-}
-
-func (r *RegistrationHandler) GetHandlers(_ cpi.Context) registrations.HandlerInfos {
- return registrations.NewLeafHandlerInfo("uploading http charts to http repositories", `
-The `+BlobHandlerName+`
uploader is able to upload artifacts to an arbitrary http path.
-If registered the default mime type is: `+mime.MIME_TGZ+` and `+mime.MIME_TGZ_ALT+`.
-
-It accepts a plain string for the URL and a config with the following field:
-'url': the URL of the http server to request.
-'method': the HTTP Method to use for the request for an individual artifact.
-''
-`)
-}
diff --git a/api/ocm/plugin/ppi/cmds/upload/put/cmd.go b/api/ocm/plugin/ppi/cmds/upload/put/cmd.go
index b679d7a797..4d5489b9a1 100644
--- a/api/ocm/plugin/ppi/cmds/upload/put/cmd.go
+++ b/api/ocm/plugin/ppi/cmds/upload/put/cmd.go
@@ -87,7 +87,17 @@ func Command(p ppi.Plugin, cmd *cobra.Command, opts *Options) error {
if u == nil {
return errors.ErrNotFound(descriptor.KIND_UPLOADER, fmt.Sprintf("%s:%s", opts.ArtifactType, opts.MediaType))
}
+
+ fi, err := os.Stdin.Stat()
+ if err != nil {
+ fmt.Println("failed to stat stdin", err)
+ }
+ if size := fi.Size(); size == 0 {
+ return fmt.Errorf("stdin is empty, and nothing can be uploaded")
+ }
+
h, err := u.Upload(p, opts.ArtifactType, opts.MediaType, opts.Hint, spec, opts.Credentials, os.Stdin)
+
if err != nil {
return fmt.Errorf("upload failed: %w", err)
}
diff --git a/cmds/jfrogplugin/uploaders/helm.go b/cmds/jfrogplugin/uploaders/helm.go
index 03b7df4ae9..2698987d3d 100644
--- a/cmds/jfrogplugin/uploaders/helm.go
+++ b/cmds/jfrogplugin/uploaders/helm.go
@@ -1,6 +1,7 @@
package uploaders
import (
+ "context"
"encoding/json"
"fmt"
"io"
@@ -8,8 +9,9 @@ import (
"net/url"
"path"
"strings"
+ "time"
- "github.com/mandelsoft/goutils/errors"
+ "github.com/containerd/containerd/reference"
"ocm.software/ocm/api/credentials"
"ocm.software/ocm/api/credentials/cpi"
@@ -21,7 +23,7 @@ import (
)
const (
- NAME = "JFrog"
+ NAME = "JFrogHelm"
VERSION = "v1"
ID_HOSTNAME = hostpath.ID_HOSTNAME
@@ -35,9 +37,8 @@ type Config struct {
func GetConfig(raw json.RawMessage) (interface{}, error) {
var cfg Config
- err := json.Unmarshal(raw, &cfg)
- if err != nil {
- return nil, err
+ if err := json.Unmarshal(raw, &cfg); err != nil {
+ return nil, fmt.Errorf("could not get config: %w", err)
}
return &cfg, nil
}
@@ -47,7 +48,6 @@ type HelmTargetSpec struct {
// URL is the hostname of the JFrog instance
URL string `json:"url"`
- url *url.URL
// Repository is the repository to upload to
Repository string `json:"repository"`
@@ -83,49 +83,66 @@ func (a *Uploader) Decoders() ppi.UploadFormats {
return types
}
-func (a *Uploader) ValidateSpecification(_ ppi.Plugin, spec ppi.UploadTargetSpec) (*ppi.UploadTargetSpecInfo, error) {
+func (a *Uploader) ValidateSpecification(p ppi.Plugin, spec ppi.UploadTargetSpec) (*ppi.UploadTargetSpecInfo, error) {
var info ppi.UploadTargetSpecInfo
my, ok := spec.(*HelmTargetSpec)
if !ok {
return nil, fmt.Errorf("invalid spec type %T", spec)
}
- var err error
- if my.url, err = url.Parse(my.URL); err != nil {
- return nil, fmt.Errorf("invalid URL: %w", err)
+ purl, err := ParseURL(my.URL)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse URL: %w", err)
}
info.ConsumerId = credentials.ConsumerIdentity{
cpi.ID_TYPE: NAME,
- ID_HOSTNAME: my.url.Hostname(),
- ID_PORT: my.url.Port(),
+ ID_HOSTNAME: purl.Hostname(),
ID_REPOSITORY: my.Repository,
}
+ if purl.Port() != "" {
+ info.ConsumerId.SetNonEmptyValue(ID_PORT, purl.Port())
+ }
+
return &info, nil
}
-func (a *Uploader) Upload(p ppi.Plugin, artifactType, mediatype, _ string, repo ppi.UploadTargetSpec, creds credentials.Credentials, reader io.Reader) (ppi.AccessSpecProvider, error) {
- cfg, err := p.GetConfig()
- if err != nil {
- return nil, errors.Wrapf(err, "can't get config for access method %s", mediatype)
- }
-
+func (a *Uploader) Upload(_ ppi.Plugin, artifactType, _, hint string, repo ppi.UploadTargetSpec, creds credentials.Credentials, reader io.Reader) (ppi.AccessSpecProvider, error) {
if artifactType != artifacttypes.HELM_CHART {
return nil, fmt.Errorf("unsupported artifact type %s", artifactType)
}
- if cfg != nil {
- _, ok := cfg.(Config)
- if !ok {
- return nil, fmt.Errorf("invalid config type %T", cfg)
+ my := repo.(*HelmTargetSpec)
+
+ if refFromHint, err := reference.Parse(hint); err == nil {
+ if refFromHint.Digest() != "" && refFromHint.Object == "" {
+ return nil, fmt.Errorf("the hint contained a valid reference but it was a digest, so it cannot be used to deduce a version of the helm chart: %s", refFromHint)
}
+ my.ChartVersion = refFromHint.Object
+ my.ChartName = path.Base(refFromHint.Locator)
}
- my := repo.(*HelmTargetSpec)
+ if my.ChartName == "" {
+ return nil, fmt.Errorf("the chart name could not be deduced from the hint (%s) or the config (%s)", hint, my)
+ }
+ if my.ChartVersion == "" {
+ return nil, fmt.Errorf("the chart version could not be deduced from the hint (%s) or the config (%s)", hint, my)
+ }
+
+ requestURL := path.Join(my.URL, "artifactory", my.Repository, fmt.Sprintf("%s-%s.tgz", my.ChartName, my.ChartVersion))
- requestURL := path.Join(my.url.String(), "artifactory", my.Repository, fmt.Sprintf("%s-%s.tgz", my.ChartName, my.ChartVersion))
+ requestURLParsed, err := ParseURL(requestURL)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse full request URL: %w", err)
+ }
+ if requestURLParsed.Scheme == "" {
+ requestURLParsed.Scheme = "https"
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
- req, err := http.NewRequest(http.MethodPost, requestURL, reader)
+ req, err := http.NewRequestWithContext(ctx, http.MethodPut, requestURLParsed.String(), reader)
if err != nil {
return nil, err
}
@@ -151,6 +168,14 @@ func (a *Uploader) Upload(p ppi.Plugin, artifactType, mediatype, _ string, repo
}
defer response.Body.Close()
+ if 200 > response.StatusCode || response.StatusCode >= 300 {
+ var body string
+ if d, err := io.ReadAll(response.Body); err == nil && len(d) > 0 {
+ body = fmt.Sprintf(": %s", string(d))
+ }
+ return nil, fmt.Errorf("invalid response (status %v)%s", response.StatusCode, body)
+ }
+
uploadResponse := &ArtifactoryUploadResponse{}
if err := json.NewDecoder(response.Body).Decode(uploadResponse); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
@@ -207,3 +232,18 @@ func (r *ArtifactoryUploadResponse) ToHelmAccessSpec() (ppi.AccessSpec, error) {
return helm.New(chart, repo), nil
}
+
+func ParseURL(urlToParse string) (*url.URL, error) {
+ const dummyScheme = "dummy"
+ if !strings.Contains(urlToParse, "://") {
+ urlToParse = dummyScheme + "://" + urlToParse
+ }
+ parsedURL, err := url.Parse(urlToParse)
+ if err != nil {
+ return nil, err
+ }
+ if parsedURL.Scheme == dummyScheme {
+ parsedURL.Scheme = ""
+ }
+ return parsedURL, nil
+}
diff --git a/components/jfrogplugin/Makefile b/components/jfrogplugin/Makefile
new file mode 100644
index 0000000000..ef51b171e2
--- /dev/null
+++ b/components/jfrogplugin/Makefile
@@ -0,0 +1,127 @@
+NAME = jfrogplugin
+PROVIDER ?= ocm.software
+GITHUBORG ?= open-component-model
+COMPONENT = $(PROVIDER)/plugins/$(NAME)
+OCMREPO ?= ghcr.io/$(GITHUBORG)/ocm
+PLATFORMS ?= linux/amd64 linux/arm64 darwin/amd64 darwin/arm64
+CTF_TYPE ?= directory
+
+
+REPO_ROOT := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))/../..
+ifeq ($(VERSION),)
+VERSION := $(shell go run ../../api/version/generate/release_generate.go print-rc-version $(CANDIDATE))
+endif
+COMMIT = $(shell git rev-parse --verify HEAD)
+# if EFFECTIVE_VERSION is not set, set it to VERSION+COMMIT
+# this is not the same as '?=' because it will also set the value if EFFECTIVE_VERSION is set to an empty string
+ifeq ($(EFFECTIVE_VERSION),)
+EFFECTIVE_VERSION := $(VERSION)+$(COMMIT)
+endif
+GIT_TREE_STATE := $(shell [ -z "$$(git status --porcelain 2>/dev/null)" ] && echo clean || echo dirty)
+
+CMDSRCS=$(shell find $(REPO_ROOT)/cmds/$(NAME) -type f)
+OCMSRCS=$(shell find $(REPO_ROOT)/api -type f) $(REPO_ROOT)/go.*
+
+CREDS ?=
+# Define the path to the binary
+OCM_BIN = $(REPO_ROOT)/bin/ocm
+
+# Rule to build the binary if it doesn't exist or if the source code has changed
+$(OCM_BIN): $(REPO_ROOT)/cmds/ocm/main.go
+ mkdir -p $(REPO_ROOT)/bin
+ go build -ldflags $(BUILD_FLAGS) -o $(OCM_BIN) $(REPO_ROOT)/cmds/ocm
+
+# Use the binary for the OCM command
+OCM = $(OCM_BIN) $(CREDS)
+
+GEN = $(REPO_ROOT)/gen/$(NAME)
+
+ $(GEN):
+ @mkdir -p $(GEN)
+
+NOW := $(shell date -u +%FT%T%z)
+BUILD_FLAGS := "-s -w \
+ -X ocm.software/ocm/api/version.gitVersion=$(EFFECTIVE_VERSION) \
+ -X ocm.software/ocm/api/version.gitTreeState=$(GIT_TREE_STATE) \
+ -X ocm.software/ocm/api/version.gitCommit=$(COMMIT) \
+ -X ocm.software/ocm/api/version.buildDate=$(NOW)"
+
+.PHONY: build
+build: $(GEN)/build
+
+$(GEN)/build: $(GEN) $(CMDSRCS) $(OCMSRCS)
+ @for i in $(PLATFORMS); do \
+ tag=$$(echo $$i | sed -e s:/:-:g); \
+ echo GOARCH=$$(basename $$i) GOOS=$$(dirname $$i) CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o $(GEN)/$(NAME).$$tag ../../cmds/$(NAME); \
+ GOARCH=$$(basename $$i) GOOS=$$(dirname $$i) CGO_ENABLED=0 go build -ldflags $(BUILD_FLAGS) -o $(GEN)/$(NAME).$$tag ../../cmds/$(NAME) & \
+ done; \
+ wait
+ @touch $(GEN)/build
+
+
+.PHONY: ctf
+ctf: $(GEN)/ctf
+
+$(GEN)/ctf: $(OCM_BIN) $(GEN)/.exists $(GEN)/build component-constructor.yaml $(CHARTSRCS)
+ @rm -rf "$(GEN)/ctf"
+ $(OCM) add componentversions \
+ --create \
+ --file $(GEN)/ctf \
+ --type $(CTF_TYPE) \
+ --templater=spiff \
+ COMPONENT="$(COMPONENT)" \
+ NAME="$(NAME)" \
+ VERSION="$(VERSION)" \
+ PROVIDER="$(PROVIDER)" \
+ COMMIT="$(COMMIT)" \
+ GEN="$(GEN)" \
+ PLATFORMS="$(PLATFORMS)" \
+ component-constructor.yaml
+ touch "$(GEN)/ctf"
+
+.PHONY: version
+version:
+ @echo $(VERSION)
+
+.PHONY: push
+push: $(GEN)/ctf $(GEN)/push.$(NAME)
+
+$(GEN)/push.$(NAME): $(GEN)/ctf $(OCM_BIN)
+ $(OCM) transfer ctf -f $(GEN)/ctf $(OCMREPO)
+ @touch $(GEN)/push.$(NAME)
+
+.PHONY: plain-push
+plain-push: $(GEN) $(OCM_BIN)
+ $(OCM) transfer ctf -f $(GEN)/ctf $(OCMREPO)
+ @touch $(GEN)/push.$(NAME)
+
+.PHONY: transport
+transport: $(OCM_BIN)
+ifneq ($(TARGETREPO),)
+ $(OCM) transfer component -Vc $(OCMREPO)//$(COMPONENT):$(VERSION) $(TARGETREPO)
+endif
+
+$(GEN)/.exists:
+ @mkdir -p $(GEN)
+ @touch $@
+
+.PHONY: info
+info:
+ @echo "ROOT: $(REPO_ROOT)"
+ @echo "VERSION: $(VERSION)"
+ @echo "COMMIT; $(COMMIT)"
+
+.PHONY: describe
+describe: $(GEN)/ctf $(OCM_BIN)
+ $(OCM) get resources --lookup $(OCMREPO) -r -o treewide $(GEN)/ctf
+
+.PHONY: descriptor
+descriptor: $(GEN)/ctf $(OCM_BIN)
+ $(OCM) get component -S v3alpha1 -o yaml $(GEN)/ctf
+
+.PHONY: clean
+clean:
+ rm -rf $(GEN)
+
+install: $(GEN)/ctf $(OCM_BIN)
+ $(OCM) install plugin -f $(GEN)/ctf
\ No newline at end of file
diff --git a/components/jfrogplugin/bindings.yaml b/components/jfrogplugin/bindings.yaml
new file mode 100644
index 0000000000..ae8fb3bd18
--- /dev/null
+++ b/components/jfrogplugin/bindings.yaml
@@ -0,0 +1,4 @@
+values:
+ GEN: ../../gen/jfrogplugin
+ PLATFORMS: linux/amd64 linux/arm64 darwin/amd64 darwin/arm64
+ VERSION: "1.0"
diff --git a/components/jfrogplugin/component-constructor.yaml b/components/jfrogplugin/component-constructor.yaml
new file mode 100644
index 0000000000..b5b7d3f51b
--- /dev/null
+++ b/components/jfrogplugin/component-constructor.yaml
@@ -0,0 +1,23 @@
+---
+helper:
+ <<<: (( &temporary ))
+ executable:
+ <<<: (( &template ))
+ name: jfrog
+ type: ocmPlugin
+ version: (( values.VERSION ))
+ extraIdentity:
+ os: ((dirname(p) ))
+ architecture: (( basename(p) ))
+ input:
+ type: file
+ # Generate the path to the plugin binary by looking into the base path and encoding the platform
+ path: (( values.GEN "/" values.NAME "." replace(p,"/","-") ))
+
+components:
+ - name: (( values.COMPONENT))
+ version: (( values.VERSION))
+ provider:
+ name: (( values.PROVIDER))
+ # use all platforms and create a resource for each
+ resources: (( map[split(" ", values.PLATFORMS)|p|-> *helper.executable] ))
\ No newline at end of file
diff --git a/components/jfrogplugin/sources.yaml b/components/jfrogplugin/sources.yaml
new file mode 100644
index 0000000000..dcc9c5f896
--- /dev/null
+++ b/components/jfrogplugin/sources.yaml
@@ -0,0 +1,7 @@
+name: source
+type: filesytem
+access:
+ type: github
+ repoUrl: github.com/open-component-model/ocm
+ commit: ${COMMIT}
+version: ${VERSION}