From ded09df80f51562a41f244dc9c2d0845844c7a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Guilherme=20Vanz?= Date: Tue, 18 Jun 2024 12:54:58 -0300 Subject: [PATCH] feat: expose capability to get OCI image configuration. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new function to allow policy author to get the OCI image manifest and configuration from a given OCI image name. Signed-off-by: José Guilherme Vanz --- .../oci/manifest_config/manifest.go | 32 +++++ .../oci/manifest_config/manifest_test.go | 115 ++++++++++++++++++ pkg/capabilities/oci/manifest_config/types.go | 11 ++ 3 files changed, 158 insertions(+) create mode 100644 pkg/capabilities/oci/manifest_config/manifest.go create mode 100644 pkg/capabilities/oci/manifest_config/manifest_test.go create mode 100644 pkg/capabilities/oci/manifest_config/types.go diff --git a/pkg/capabilities/oci/manifest_config/manifest.go b/pkg/capabilities/oci/manifest_config/manifest.go new file mode 100644 index 0000000..2a7dfdf --- /dev/null +++ b/pkg/capabilities/oci/manifest_config/manifest.go @@ -0,0 +1,32 @@ +package manifest_config + +import ( + "encoding/json" + "errors" + "fmt" + + cap "github.com/kubewarden/policy-sdk-go/pkg/capabilities" +) + +// GetOCIManifestAndConfig fetches the OCI manifest and configuration for the given image URI. +// Arguments: +// * image: image to be verified (e.g.: `registry.testing.lan/busybox:1.0.0`) +func GetOCIManifestAndConfig(h *cap.Host, image string) (*OciImageManifestAndConfigResponse, error) { + // build request payload, e.g: `"ghcr.io/kubewarden/policies/pod-privileged:v0.1.10"` + payload, err := json.Marshal(image) + if err != nil { + return nil, fmt.Errorf("cannot serialize image URI to JSON: %w", err) + } + + // perform host callback + responsePayload, err := h.Client.HostCall("kubewarden", "oci", "v1/oci_manifest_config", payload) + if err != nil { + return nil, err + } + + response := OciImageManifestAndConfigResponse{} + if err := json.Unmarshal(responsePayload, &response); err != nil { + return nil, errors.Join(fmt.Errorf("failed to parse response from the host"), err) + } + return &response, nil +} diff --git a/pkg/capabilities/oci/manifest_config/manifest_test.go b/pkg/capabilities/oci/manifest_config/manifest_test.go new file mode 100644 index 0000000..7e4e69c --- /dev/null +++ b/pkg/capabilities/oci/manifest_config/manifest_test.go @@ -0,0 +1,115 @@ +package manifest_config + +import ( + _ "crypto/sha256" + "encoding/json" + "testing" + "time" + + cap "github.com/kubewarden/policy-sdk-go/pkg/capabilities" + digest "github.com/opencontainers/go-digest" + specs "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/google/go-cmp/cmp" + "github.com/kubewarden/policy-sdk-go/pkg/capabilities/mocks" +) + +func buildHostMock(imageURI string, returnPayload []byte) (*cap.Host, error) { + mockWapcClient := &mocks.MockWapcClient{} + expectedPayload, err := json.Marshal(imageURI) + if err != nil { + return nil, err + } + mockWapcClient.EXPECT().HostCall("kubewarden", "oci", "v1/oci_manifest_config", expectedPayload).Return(returnPayload, nil).Times(1) + return &cap.Host{ + Client: mockWapcClient, + }, nil +} + +func buildManifestAndConfigResponse() interface{} { + manifest := specs.Manifest{ + MediaType: specs.MediaTypeImageManifest, + Config: specs.Descriptor{ + MediaType: specs.MediaTypeDescriptor, + Digest: digest.FromString("mydummydigest"), + Size: 1024, + URLs: []string{"ghcr.io/kubewarden/policy-server:latest"}, + Annotations: map[string]string{"annotation": "value"}, + Platform: &specs.Platform{ + Architecture: "amd64", + OS: "linux", + }, + }, + Layers: []specs.Descriptor{}, + Annotations: map[string]string{"annotation": "value"}, + } + now := time.Now() + image := specs.Image{ + Created: &now, + Author: "kubewarden", + Platform: specs.Platform{ + Architecture: "amd64", + OS: "linux", + OSVersion: "1.0.0", + OSFeatures: []string{"feature1", "feature2"}, + Variant: "variant", + }, + Config: specs.ImageConfig{ + User: "1000", + Cmd: []string{"echo", "hello"}, + Entrypoint: []string{"echo"}, + Env: []string{"key=value"}, + WorkingDir: "/", + Labels: map[string]string{"label": "value"}, + StopSignal: "SIGTERM", + ExposedPorts: map[string]struct{}{"80/tcp": {}}, + Volumes: map[string]struct{}{"/tmp": {}}, + ArgsEscaped: true, + }, + RootFS: specs.RootFS{ + Type: "layers", + DiffIDs: []digest.Digest{digest.FromString("mydummydigest")}, + }, + History: []specs.History{ + { + Created: &now, + CreatedBy: "kubewarden", + Author: "kubewarden", + Comment: "initial commit", + EmptyLayer: false, + }, + }, + } + return OciImageManifestAndConfigResponse{ + Manifest: &manifest, + Digest: "mydummydigest", + ImageConfig: &image, + } +} + +func TestOciManifestAndConfig(t *testing.T) { + manifest := buildManifestAndConfigResponse() + manifestPayload, err := json.Marshal(manifest) + if err != nil { + t.Fatalf("cannot serialize response object: %v", err) + } + response := OciImageManifestAndConfigResponse{} + if err := json.Unmarshal(manifestPayload, &response); err != nil { + t.Fatalf("failed to parse response from the host") + } + + imageURI := "myimage:latest" + host, err := buildHostMock(imageURI, manifestPayload) + if err != nil { + t.Fatalf("cannot build host mock: %q", err) + } + + res, err := GetOCIManifestAndConfig(host, imageURI) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if diff := cmp.Diff(*res, manifest); diff != "" { + t.Fatalf("invalid manifest and config response:\n%s", diff) + } +} diff --git a/pkg/capabilities/oci/manifest_config/types.go b/pkg/capabilities/oci/manifest_config/types.go new file mode 100644 index 0000000..531c027 --- /dev/null +++ b/pkg/capabilities/oci/manifest_config/types.go @@ -0,0 +1,11 @@ +package manifest_config + +import ( + specs "github.com/opencontainers/image-spec/specs-go/v1" +) + +type OciImageManifestAndConfigResponse struct { + Manifest *specs.Manifest `json:"manifest"` + Digest string `json:"digest"` + ImageConfig *specs.Image `json:"config"` +}