1
0
Fork 0
mirror of https://github.com/kubernetes-sigs/node-feature-discovery.git synced 2025-03-06 16:57:10 +00:00
node-feature-discovery/pkg/client-nfd/compat/artifact-client/client.go
Marcin Franczyk 241c886bf9
Sort the list of compatibility artifacts in desc order
Signed-off-by: Marcin Franczyk <marcin0franczyk@gmail.com>
2025-01-08 14:00:09 +01:00

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 }}
}