diff --git a/app/controlplane/plugins/core/oci-registry/v1/README.md b/app/controlplane/plugins/core/oci-registry/v1/README.md deleted file mode 100644 index 7ba32c309..000000000 --- a/app/controlplane/plugins/core/oci-registry/v1/README.md +++ /dev/null @@ -1,123 +0,0 @@ -# OCI registry plugin - -Send attestations to a compatible OCI repository. - -## How to use it - -1. To get started, you need to register the plugin in your Chainloop organization. - -```console -$ chainloop integration registered add oci-registry --opt repository=[repo] --opt username=[username] --opt password=[password] -``` - -2. When attaching the integration to your workflow, you have the option to specify an image name prefix: - -```console -chainloop integration attached add --workflow $WID --integration $IID --opt prefix=custom-prefix -``` - -## Examples different providers - -See below a non-exhaustive list of examples for different OCI registry providers known to work well with this plugin. - -### Google Artifact Registry - -Using json-based service account https://console.cloud.google.com/iam-admin/serviceaccounts - -```console -$ chainloop integration registered add oci-registry \ - # i.e us-east1-docker.pkg.dev/my-project/chainloop-cas-devel - --opt repository=[region]-docker.pkg.dev/[my-project]/[my-repository] \ - --opt username=_json_key \ - --opt "password=$(cat service-account.json)" -``` - -### GitHub packages - -Using personal access token with write:packages permissions https://github.com/settings/tokens - -```console -$ chainloop integration registered add oci-registry \ - # i.e ghcr.io/chainloop-dev/chainloop-cas - --opt repository=ghcr.io/[username or org]/[my-repository] \ - --opt username=[username] \ - --opt password=[personal access token] -``` - -### DockerHub - -Create a personal access token at https://hub.docker.com/settings/security - -```console -$ chainloop integration registered add oci-registry \ - --opt repository=index.docker.io/[username] \ - --opt username=[username] \ - --opt password=[personal access token] -``` - -### AWS Container Registry - -Not supported at the moment - - - -## Registration Input Schema - -|Field|Type|Required|Description| -|---|---|---|---| -|password|string|yes|OCI repository password| -|repository|string|yes|OCI repository uri and path| -|username|string|yes|OCI repository username| - -```json -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/chainloop-dev/chainloop/app/controlplane/plugins/core/oci-registry/v1/registration-request", - "properties": { - "repository": { - "type": "string", - "minLength": 1, - "description": "OCI repository uri and path" - }, - "username": { - "type": "string", - "minLength": 1, - "description": "OCI repository username" - }, - "password": { - "type": "string", - "minLength": 1, - "description": "OCI repository password" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "repository", - "username", - "password" - ] -} -``` - -## Attachment Input Schema - -|Field|Type|Required|Description| -|---|---|---|---| -|prefix|string|no|OCI images name prefix (default chainloop)| - -```json -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/chainloop-dev/chainloop/app/controlplane/plugins/core/oci-registry/v1/attachment-request", - "properties": { - "prefix": { - "type": "string", - "minLength": 1, - "description": "OCI images name prefix (default chainloop)" - } - }, - "additionalProperties": false, - "type": "object" -} -``` \ No newline at end of file diff --git a/app/controlplane/plugins/core/oci-registry/v1/ociregistry.go b/app/controlplane/plugins/core/oci-registry/v1/ociregistry.go deleted file mode 100644 index 913097b1e..000000000 --- a/app/controlplane/plugins/core/oci-registry/v1/ociregistry.go +++ /dev/null @@ -1,221 +0,0 @@ -// -// Copyright 2023 The Chainloop Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ociregistry - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - - v1 "github.com/chainloop-dev/chainloop/app/artifact-cas/api/cas/v1" - "github.com/chainloop-dev/chainloop/app/controlplane/plugins/sdk/v1" - "github.com/chainloop-dev/chainloop/internal/blobmanager/oci" - "github.com/chainloop-dev/chainloop/internal/ociauth" - "github.com/go-kratos/kratos/v2/log" - cr_v1 "github.com/google/go-containerregistry/pkg/v1" -) - -type Integration struct { - *sdk.FanOutIntegration -} - -// 1 - API schema definitions -type registrationRequest struct { - // Repository is not fully URI compliant and hence can not be validated with jsonschema - Repository string `json:"repository" jsonschema:"minLength=1,description=OCI repository uri and path"` - Username string `json:"username" jsonschema:"minLength=1,description=OCI repository username"` - Password string `json:"password" jsonschema:"minLength=1,description=OCI repository password"` -} - -type attachmentRequest struct { - Prefix string `json:"prefix,omitempty" jsonschema:"minLength=1,description=OCI images name prefix (default chainloop)"` -} - -// 2 - Configuration state -type registrationState struct { - Repository string `json:"repository"` -} - -type attachmentState struct { - Prefix string `json:"prefix"` -} - -func New(l log.Logger) (sdk.FanOut, error) { - base, err := sdk.NewFanOut( - &sdk.NewParams{ - ID: "oci-registry", - Version: "1.0", - Description: "Send attestations to a compatible OCI registry", - Logger: l, - InputSchema: &sdk.InputSchema{ - Registration: ®istrationRequest{}, - Attachment: &attachmentRequest{}, - }, - }, - ) - - if err != nil { - return nil, err - } - - return &Integration{base}, nil -} - -// Register is executed when a operator wants to register a specific instance of this integration with their Chainloop organization -func (i *Integration) Register(_ context.Context, req *sdk.RegistrationRequest) (*sdk.RegistrationResponse, error) { - i.Logger.Info("registration requested") - - // Extract request payload - var request *registrationRequest - if err := sdk.FromConfig(req.Payload, &request); err != nil { - return nil, fmt.Errorf("invalid registration request: %w", err) - } - - // Create and validate OCI credentials - k, err := ociauth.NewCredentials(request.Repository, request.Username, request.Password) - if err != nil { - return nil, fmt.Errorf("the provided credentials are invalid") - } - - // Check write permissions - b, err := oci.NewBackend(request.Repository, &oci.RegistryOptions{Keychain: k}) - if err != nil { - return nil, fmt.Errorf("the provided credentials are invalid") - } - - if err := b.CheckWritePermissions(context.TODO()); err != nil { - return nil, fmt.Errorf("the provided credentials don't have write permissions") - } - - // They seem valid, let's store them in the configuration and credentials state - response := &sdk.RegistrationResponse{} - - // a) Configuration State - rawConfig, err := sdk.ToConfig(®istrationState{ - Repository: request.Repository, - }) - if err != nil { - return nil, fmt.Errorf("marshalling configuration: %w", err) - } - - response.Configuration = rawConfig - - // b) Credentials state - response.Credentials = &sdk.Credentials{Password: request.Password, Username: request.Username} - - return response, nil -} - -// Attachment is executed when to attach a registered instance of this integration to a specific workflow -func (i *Integration) Attach(_ context.Context, req *sdk.AttachmentRequest) (*sdk.AttachmentResponse, error) { - // Extract request payload - var request *attachmentRequest - if err := sdk.FromConfig(req.Payload, &request); err != nil { - return nil, fmt.Errorf("invalid registration request: %w", err) - } - - // Define the state to be stored - config, err := sdk.ToConfig(&attachmentState{Prefix: request.Prefix}) - if err != nil { - return nil, fmt.Errorf("marshalling configuration: %w", err) - } - - return &sdk.AttachmentResponse{Configuration: config}, nil -} - -func (i *Integration) Execute(ctx context.Context, req *sdk.ExecutionRequest) error { - i.Logger.Info("execution requested") - - if err := validateExecuteRequest(req); err != nil { - return fmt.Errorf("running validation: %w", err) - } - - // Extract registration configuration and credentials - var registrationConfig *registrationState - if err := sdk.FromConfig(req.RegistrationInfo.Configuration, ®istrationConfig); err != nil { - return fmt.Errorf("invalid registration configuration %w", err) - } - - // Extract attachment configuration - var attachmentConfig *attachmentState - if err := sdk.FromConfig(req.AttachmentInfo.Configuration, &attachmentConfig); err != nil { - return fmt.Errorf("invalid attachment configuration %w", err) - } - - // Create OCI backend client - credentials := req.RegistrationInfo.Credentials - k, err := ociauth.NewCredentials(registrationConfig.Repository, credentials.Username, credentials.Password) - if err != nil { - return fmt.Errorf("setting up the keychain: %w", err) - } - - // Add prefix if provided - var opts = make([]oci.NewBackendOpt, 0) - if attachmentConfig.Prefix != "" { - opts = append(opts, oci.WithPrefix(attachmentConfig.Prefix)) - } - - ociClient, err := oci.NewBackend(registrationConfig.Repository, &oci.RegistryOptions{Keychain: k}, opts...) - if err != nil { - return fmt.Errorf("creating OCI backend %w", err) - } - - i.Logger.Infow("msg", "Uploading attestation", "repo", registrationConfig.Repository, "workflowID", req.Workflow.ID) - - // Perform the upload of the json marshalled attestation - jsonContent, err := json.Marshal(req.Input.Attestation.Envelope) - if err != nil { - return fmt.Errorf("marshaling the envelope: %w", err) - } - - // Calculate digest since it will be used as CAS reference - h, _, err := cr_v1.SHA256(bytes.NewBuffer(jsonContent)) - if err != nil { - return fmt.Errorf("calculating the digest: %w", err) - } - - if err := ociClient.Upload(ctx, bytes.NewBuffer(jsonContent), &v1.CASResource{Digest: h.Hex, FileName: "attestation.json"}); err != nil { - return fmt.Errorf("uploading the attestation: %w", err) - } - - i.Logger.Infow("msg", "Attestation uploaded", "repo", registrationConfig.Repository, "workflowID", req.Workflow.ID) - - return nil -} - -// Validate that we are receiving an envelope -// and the credentials and state from the registration stage -func validateExecuteRequest(req *sdk.ExecutionRequest) error { - if req == nil || req.Input == nil { - return errors.New("execution input not received") - } - - if req.Input.Attestation == nil { - return errors.New("execution input invalid, the envelope is empty") - } - - if req.RegistrationInfo == nil || req.RegistrationInfo.Configuration == nil { - return errors.New("missing registration configuration") - } - - if req.RegistrationInfo.Credentials == nil { - return errors.New("missing credentials") - } - - return nil -} diff --git a/app/controlplane/plugins/core/oci-registry/v1/ociregistry_test.go b/app/controlplane/plugins/core/oci-registry/v1/ociregistry_test.go deleted file mode 100644 index 4c9acacf0..000000000 --- a/app/controlplane/plugins/core/oci-registry/v1/ociregistry_test.go +++ /dev/null @@ -1,128 +0,0 @@ -// -// Copyright 2023 The Chainloop Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package ociregistry - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestValidateRegistrationInput(t *testing.T) { - testCases := []struct { - name string - input map[string]interface{} - errMsg string - }{ - { - name: "not ok, missing properties", - input: map[string]interface{}{}, - errMsg: "missing properties: 'repository', 'username', 'password'", - }, - { - name: "not ok, random properties", - input: map[string]interface{}{"foo": "bar"}, - errMsg: "additionalProperties 'foo' not allowed", - }, - { - name: "ok, all properties", - input: map[string]interface{}{"repository": "repo.io", "username": "u", "password": "p"}, - }, - { - name: "not ok, empty repository", - input: map[string]interface{}{"repository": "", "username": "u", "password": "p"}, - errMsg: "length must be >= 1, but got 0", - }, - { - name: "not ok, empty username", - input: map[string]interface{}{"repository": "r", "username": "", "password": "p"}, - errMsg: "length must be >= 1, but got 0", - }, - { - name: "not ok, empty password", - input: map[string]interface{}{"repository": "r", "username": "u", "password": ""}, - errMsg: "length must be >= 1, but got 0", - }, - } - - integration, err := New(nil) - require.NoError(t, err) - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - payload, err := json.Marshal(tc.input) - require.NoError(t, err) - - err = integration.ValidateRegistrationRequest(payload) - if tc.errMsg != "" { - assert.ErrorContains(t, err, tc.errMsg) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestValidateAttachmentInput(t *testing.T) { - testCases := []struct { - name string - input map[string]interface{} - errMsg string - }{ - { - name: "ok, no properties", - input: map[string]interface{}{}, - }, - { - name: "not ok, random properties", - input: map[string]interface{}{"foo": "bar"}, - errMsg: "additionalProperties 'foo' not allowed", - }, - { - name: "not ok, set but empty prefix", - input: map[string]interface{}{"prefix": ""}, - errMsg: "length must be >= 1, but got 0", - }, - { - name: "valid request", - input: map[string]interface{}{"prefix": "p"}, - }, - } - - integration, err := New(nil) - require.NoError(t, err) - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - payload, err := json.Marshal(tc.input) - require.NoError(t, err) - - err = integration.ValidateAttachmentRequest(payload) - if tc.errMsg != "" { - assert.ErrorContains(t, err, tc.errMsg) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestNewIntegration(t *testing.T) { - _, err := New(nil) - assert.NoError(t, err) -} diff --git a/app/controlplane/plugins/plugins.go b/app/controlplane/plugins/plugins.go index 00ae90805..f5ae7a1cd 100644 --- a/app/controlplane/plugins/plugins.go +++ b/app/controlplane/plugins/plugins.go @@ -23,7 +23,6 @@ import ( dependencytrack "github.com/chainloop-dev/chainloop/app/controlplane/plugins/core/dependency-track/v1" "github.com/chainloop-dev/chainloop/app/controlplane/plugins/core/discord-webhook/v1" "github.com/chainloop-dev/chainloop/app/controlplane/plugins/core/guac/v1" - ociregistry "github.com/chainloop-dev/chainloop/app/controlplane/plugins/core/oci-registry/v1" "github.com/chainloop-dev/chainloop/app/controlplane/plugins/core/slack-webhook/v1" "github.com/chainloop-dev/chainloop/app/controlplane/plugins/core/smtp/v1" "github.com/chainloop-dev/chainloop/app/controlplane/plugins/sdk/v1" @@ -68,7 +67,6 @@ func Load(pluginsDir string, l log.Logger) (plugins sdk.AvailablePlugins, err er toEnableBuiltIn := []sdk.FanOutFactory{ dependencytrack.New, smtp.New, - ociregistry.New, discord.New, guac.New, slack.New, diff --git a/docs/integrations.md b/docs/integrations.md index c1f0e5421..81925ed88 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -13,7 +13,6 @@ Below you can find the list of currently available integrations. If you can't fi | [dependency-track](https://github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core/dependency-track/v1/README.md) | 1.3 | Send CycloneDX SBOMs to your Dependency-Track instance | SBOM_CYCLONEDX_JSON | | [discord-webhook](https://github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core/discord-webhook/v1/README.md) | 1.1 | Send attestations to Discord | | | [guac](https://github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core/guac/v1/README.md) | 1.0 | Export Attestation and SBOMs metadata to a blob storage backend so guacsec/guac can consume it | SBOM_CYCLONEDX_JSON, SBOM_SPDX_JSON | -| [oci-registry](https://github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core/oci-registry/v1/README.md) | 1.0 | Send attestations to a compatible OCI registry | | | [slack-webhook](https://github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core/slack-webhook/v1/README.md) | 1.0 | Send attestations to Slack | | | [smtp](https://github.com/chainloop-dev/chainloop/blob/main/app/controlplane/plugins/core/smtp/v1/README.md) | 1.0 | Send emails with information about a received attestation | |