mirror of
https://github.com/kubernetes-sigs/node-feature-discovery.git
synced 2025-03-06 16:57:10 +00:00
192 lines
5.5 KiB
Go
192 lines
5.5 KiB
Go
/*
|
|
Copyright 2024 The Kubernetes 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 compat
|
|
|
|
//go:generate mockery --name=ArtifactClient --inpackage
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"time"
|
|
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
oras "oras.land/oras-go/v2"
|
|
"oras.land/oras-go/v2/registry"
|
|
"oras.land/oras-go/v2/registry/remote"
|
|
"oras.land/oras-go/v2/registry/remote/auth"
|
|
"oras.land/oras-go/v2/registry/remote/retry"
|
|
"sigs.k8s.io/yaml"
|
|
|
|
compatv1alpha1 "sigs.k8s.io/node-feature-discovery/api/image-compatibility/v1alpha1"
|
|
)
|
|
|
|
const (
|
|
ArtifactCreationTimestampKey = "org.opencontainers.image.created"
|
|
)
|
|
|
|
// ArtifactClient interface contain set of functions to manipulate compatibility artfact.
|
|
type ArtifactClient interface {
|
|
// FetchCompatibilitySpec downloads the compatibility specifcation associated with the image.
|
|
FetchCompatibilitySpec(ctx context.Context) (*compatv1alpha1.Spec, error)
|
|
}
|
|
|
|
// Args holds command line arguments.
|
|
type Args struct {
|
|
PlainHttp bool
|
|
}
|
|
|
|
// Client represents a client that is reposnible for all artifact operations.
|
|
type Client struct {
|
|
Args Args
|
|
RegReference *registry.Reference
|
|
Platform *ocispec.Platform
|
|
|
|
orasClient *auth.Client
|
|
}
|
|
|
|
// New returns a new compatibility spec object.
|
|
func New(regReference *registry.Reference, opts ...ArtifactClientOpts) *Client {
|
|
c := &Client{
|
|
RegReference: regReference,
|
|
}
|
|
for _, opt := range opts {
|
|
opt.apply(c)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// FetchCompatibilitySpec pulls the image compatibility specification associated with the image.
|
|
func (c *Client) FetchCompatibilitySpec(ctx context.Context) (*compatv1alpha1.Spec, error) {
|
|
repo, err := remote.NewRepository(c.RegReference.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
repo.Client = c.orasClient
|
|
repo.PlainHTTP = c.Args.PlainHttp
|
|
|
|
opts := oras.DefaultResolveOptions
|
|
if c.Platform != nil {
|
|
opts.TargetPlatform = c.Platform
|
|
}
|
|
|
|
targetDesc, err := oras.Resolve(ctx, repo, c.RegReference.Reference, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
descs, err := registry.Referrers(ctx, repo, targetDesc, compatv1alpha1.ArtifactType)
|
|
if err != nil {
|
|
return nil, nil
|
|
} else if len(descs) < 1 {
|
|
return nil, fmt.Errorf("compatibility artifact not found")
|
|
}
|
|
|
|
// Sort the artifacts in desc order.
|
|
// If the artifact does not have creation timestamp it will be moved to the top of the slice.
|
|
slices.SortFunc(descs, func(i, j ocispec.Descriptor) int {
|
|
it, _ := time.Parse(time.RFC3339, i.Annotations[ArtifactCreationTimestampKey])
|
|
jt, _ := time.Parse(time.RFC3339, j.Annotations[ArtifactCreationTimestampKey])
|
|
return it.Compare(jt)
|
|
})
|
|
artifactDesc := descs[len(descs)-1]
|
|
|
|
_, content, err := oras.FetchBytes(ctx, repo.Manifests(), artifactDesc.Digest.String(), oras.DefaultFetchBytesOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
manifest := ocispec.Manifest{}
|
|
if err := json.Unmarshal(content, &manifest); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO: now it's a lazy check, verify in the future the media types and number of layers
|
|
if len(manifest.Layers) < 1 {
|
|
return nil, fmt.Errorf("compatibility layer not found")
|
|
}
|
|
specDesc := manifest.Layers[0]
|
|
|
|
_, compatSpecRaw, err := oras.FetchBytes(ctx, repo.Blobs(), specDesc.Digest.String(), oras.DefaultFetchBytesOptions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
compatSpec := compatv1alpha1.Spec{}
|
|
err = yaml.Unmarshal(compatSpecRaw, &compatSpec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &compatSpec, nil
|
|
}
|
|
|
|
// NodeValidatorOpts applies certain options to the node validator.
|
|
type ArtifactClientOpts interface {
|
|
apply(*Client)
|
|
}
|
|
|
|
type artifactClientOpt struct {
|
|
f func(*Client)
|
|
}
|
|
|
|
func (o *artifactClientOpt) apply(nv *Client) {
|
|
o.f(nv)
|
|
}
|
|
|
|
// WithArgs applies arguments to the artifact client.
|
|
func WithArgs(args Args) ArtifactClientOpts {
|
|
return &artifactClientOpt{f: func(c *Client) { c.Args = args }}
|
|
}
|
|
|
|
// WithPlatform applies OCI platform spec to the artifact client.
|
|
func WithPlatform(platform *ocispec.Platform) ArtifactClientOpts {
|
|
return &artifactClientOpt{f: func(c *Client) { c.Platform = platform }}
|
|
}
|
|
|
|
// WithAuthPassword initializes oras client with user and password.
|
|
func WithAuthPassword(username, password string) ArtifactClientOpts {
|
|
return &artifactClientOpt{f: func(c *Client) {
|
|
c.orasClient = &auth.Client{
|
|
Client: retry.DefaultClient,
|
|
Cache: auth.NewCache(),
|
|
Credential: auth.StaticCredential(c.RegReference.Registry, auth.Credential{
|
|
Username: username,
|
|
Password: password,
|
|
}),
|
|
}
|
|
}}
|
|
}
|
|
|
|
// WithAuthToken initializes oras client with auth token.
|
|
func WithAuthToken(token string) ArtifactClientOpts {
|
|
return &artifactClientOpt{f: func(c *Client) {
|
|
c.orasClient = &auth.Client{
|
|
Client: retry.DefaultClient,
|
|
Cache: auth.NewCache(),
|
|
Credential: auth.StaticCredential(c.RegReference.Registry, auth.Credential{
|
|
AccessToken: token,
|
|
}),
|
|
}
|
|
}}
|
|
}
|
|
|
|
// WithAuthDefault initializes the default oras client that does not authenticate.
|
|
func WithAuthDefault() ArtifactClientOpts {
|
|
return &artifactClientOpt{f: func(c *Client) { c.orasClient = auth.DefaultClient }}
|
|
}
|