Skip to content

Commit

Permalink
Example illustrating how an application obtains snapshot metadata.
Browse files Browse the repository at this point in the history
  • Loading branch information
carlbraganza committed Nov 10, 2024
1 parent 5d7fa9f commit de656fd
Show file tree
Hide file tree
Showing 11 changed files with 1,610 additions and 2 deletions.
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,17 @@ crd:
lint:
golangci-lint run


# Include release-tools

CMDS=csi-snapshot-metadata

include release-tools/build.make

.PHONY: examples
examples:
mkdir -p bin
for d in ./examples/* ; do if [[ -f $$d/main.go ]]; then (cd $$d && go build $(GOFLAGS_VENDOR) -a -ldflags '$(FULL_LDFLAGS)' -o "$(abspath ./bin)/" .); fi; done

# Eventually extend the test target to include lint.
# Currently the linter is not available in the CI infrastructure.
#test: lint
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
# external-snapshot-metadata

This repo contains sidecar controller for the snapshot metadata service.
This repository contains components that implement the
[KEP-3314 CSI Changed Block Tracking](https://github.com/kubernetes/enhancements/tree/master/keps/sig-storage/3314-csi-changed-block-tracking)
feature.

## Overview

The repository provides the following components:

- The [External Snapshot Metadata sidecar](https://github.com/kubernetes/enhancements/tree/master/keps/sig-storage/3314-csi-changed-block-tracking#the-external-snapshot-metadata-sidecar)
container that exposes the
[Kubernetes SnapshotMetadataService API](https://github.com/kubernetes/enhancements/tree/master/keps/sig-storage/3314-csi-changed-block-tracking#the-kubernetes-snapshotmetadata-service-api)
to backup applications, and interacts with a CSI driver's implementation
of the
[CSI SnapshotMetadataService](https://github.com/container-storage-interface/spec/blob/master/spec.md#snapshot-metadata-service-rpcs).
- The [SnapshotMetadataService CRD](https://github.com/kubernetes-csi/external-snapshot-metadata/tree/master/client/config/crd)
used to advertise the CSI Driver's support for the
CSI Changed Block Tracking feature.
- gRPC stubs
- [Kubernetes SnapshotMetadataService API](https://github.com/kubernetes-csi/external-snapshot-metadata/tree/master/pkg/api)
- [Mocks for the Kubernetes SnapshotMetadataService API client](https://github.com/kubernetes-csi/external-snapshot-metadata/tree/master/pkg/k8sclientmocks)
- [Mocks for the CSI SnapshotMetadataService API client](https://github.com/kubernetes-csi/external-snapshot-metadata/tree/master/pkg/csiclientmocks)
- Examples
- [Instructions on how to deploy a CSI driver with the `external-snapshot-metadata` sidecar](https://github.com/kubernetes-csi/external-snapshot-metadata/tree/main/deploy).
- A [snapshot-metadata-lister](https://github.com/kubernetes-csi/external-snapshot-metadata/tree/master/examples/snapshot-metadata-lister)
example command to illustrate how a Kubernetes backup application could fetch snapshot metadata.

## Community, discussion, contribution, and support

Expand Down
154 changes: 154 additions & 0 deletions examples/snapshot-metadata-lister/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
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 main

import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"

"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"

"github.com/kubernetes-csi/external-snapshot-metadata/pkg/iterator"
)

const (
shortUsageFmt = `Usage:
1. Display allocated block metadata
%[1]s -n Namespace -s Snapshot [Additional flags ...]
2. Display changed block metadata
%[1]s -n Namespace -s Snapshot -p PreviousSnapshot [Additional flags ...]
3. Display the full help message
%[1]s -h
`
usageFmt = `This command displays metadata on the content of a VolumeSnapshot object.
If a previous VolumeSnapshot object is also specified then the metadata
describes the content changes between the two snapshots, which must both
be from the same PersistentVolume.
` + shortUsageFmt + `
Flags:
`
)

// globals set by flags
var (
args iterator.Args
kubeConfig string
outputFormat string
)

func parseFlags() {
stringFlag := func(sVar *string, longName, shortName, defaultValue, description string) {
flag.StringVar(sVar, longName, defaultValue, description)
flag.StringVar(sVar, shortName, defaultValue, fmt.Sprintf("Shorthand for -%s", longName))
}

stringFlag(&args.Namespace, "namespace", "n", "", "The Namespace containing the VolumeSnapshot objects.")
stringFlag(&args.SnapshotName, "snapshot", "s", "", "The name of the VolumeSnapshot for which metadata is to be displayed.")
stringFlag(&args.PrevSnapshotName, "previous-snapshot", "p", "", "The name of an earlier VolumeSnapshot against which changed block metadata is to be displayed.")
stringFlag(&outputFormat, "output-format", "o", "table", "The format of the output. Possible values: \"table\" or \"json\".")

if home := homedir.HomeDir(); home != "" {
flag.StringVar(&kubeConfig, "kubeconfig", filepath.Join(home, ".kube", "config"), "Path to the kubeconfig file.")
} else {
flag.StringVar(&kubeConfig, "kubeconfig", "", "Path to the kubeconfig file.")
}

flag.StringVar(&args.ServiceAccount, "service-account", "default", "ServiceAccount used to create a security token.")

flag.Int64Var(&args.TokenExpirySecs, "token-expiry", 600, "Expiry time in seconds for the security token.")
flag.Int64Var(&args.StartingOffset, "starting-offset", 0, "The starting byte offset.")

var maxResults int
flag.IntVar(&maxResults, "max-results", 0, "The maximum results per record.")

var showHelp bool
flag.BoolVar(&showHelp, "h", false, "Show the full usage message.")

progName := filepath.Base(os.Args[0])
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "\n"+shortUsageFmt, progName)
}

if len(os.Args) > 1 {
flag.Parse()
args.MaxResults = int32(maxResults)
} else {
fmt.Fprintf(os.Stderr, "Missing required arguments\n")
fmt.Fprintf(os.Stderr, shortUsageFmt, progName)
os.Exit(1)
}

if showHelp {
fmt.Fprintf(os.Stderr, usageFmt, progName)
flag.PrintDefaults()
os.Exit(0)
}

switch outputFormat {
case "table":
args.Emitter = &iterator.TableEmitter{Writer: os.Stdout}
case "json":
args.Emitter = &iterator.JSONEmitter{Writer: os.Stdout}
default:
fmt.Fprintf(os.Stderr, "Unknown output format: %s\n", outputFormat)
flag.Usage()
os.Exit(1)
}
}

func main() {
// parse flags and set the output emitter
parseFlags()

// get the K8s config from either kubeConfig, in-cluster or default
config, err := clientcmd.BuildConfigFromFlags("", kubeConfig)
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading kubeconfig %s: %v\n", kubeConfig, err)
os.Exit(1)
}

clients, err := iterator.BuildClients(config)
if err != nil {
fmt.Fprintf(os.Stderr, "Error creating clients: %v\n", err)
os.Exit(1)
}

args.Clients = clients

ctx, stopFn := signal.NotifyContext(context.Background(), os.Interrupt)
defer stopFn()

if err := iterator.GetSnapshotMetadata(ctx, args); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}

os.Exit(0)
}
7 changes: 7 additions & 0 deletions pkg/internal/runtime/test_harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,13 @@ func (th *TestHarness) RemoveFakeKubeConfig(t *testing.T) {
utiltesting.CloseAndRemove(t, th.fakeKubeConfigFile)
}

func (th *TestHarness) FakeKubeConfigFileName() string {
if th.fakeKubeConfigFile != nil {
return th.fakeKubeConfigFile.Name()
}
return ""
}

// WithTestTLSFiles will provide temporary but valid TLS files.
func (th *TestHarness) WithTestTLSFiles(t *testing.T) *TestHarness {
th.tlsGenerator = &testTLSCertGenerator{}
Expand Down
73 changes: 73 additions & 0 deletions pkg/iterator/clients.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
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 iterator

import (
"fmt"

snapshot "github.com/kubernetes-csi/external-snapshotter/client/v8/clientset/versioned"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"

smsCR "github.com/kubernetes-csi/external-snapshot-metadata/client/clientset/versioned"
)

type Clients struct {
KubeClient kubernetes.Interface
SnapshotClient snapshot.Interface
SmsCRClient smsCR.Interface
}

func (c Clients) Validate() error {
switch {
case c.KubeClient == nil:
return fmt.Errorf("%w: missing KubeClient", ErrInvalidArgs)
case c.SnapshotClient == nil:
return fmt.Errorf("%w: missing SnapshotClient", ErrInvalidArgs)
case c.SmsCRClient == nil:
return fmt.Errorf("%w: missing SmsCRClient", ErrInvalidArgs)
}

return nil
}

// BuildClients constructs the necessary client interfaces from
// the given configuration.
func BuildClients(config *rest.Config) (Clients, error) {
var clients Clients

kubeClient, err := kubernetes.NewForConfig(config)
if err != nil {
return clients, fmt.Errorf("kubernetes.NewForConfig: %w", err)
}

snapshotClient, err := snapshot.NewForConfig(config)
if err != nil {
return clients, fmt.Errorf("snapshot.NewForConfig: %w", err)
}

smsClient, err := smsCR.NewForConfig(config)
if err != nil {
return clients, fmt.Errorf("SnapshotMetadataServiceCR.NewForConfig: %w", err)
}

clients.KubeClient = kubeClient
clients.SnapshotClient = snapshotClient
clients.SmsCRClient = smsClient

return clients, nil
}
41 changes: 41 additions & 0 deletions pkg/iterator/clients_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
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 iterator

import (
"testing"

"github.com/stretchr/testify/assert"
"k8s.io/client-go/tools/clientcmd"

"github.com/kubernetes-csi/external-snapshot-metadata/pkg/internal/runtime"
)

func TestBuildClients(t *testing.T) {
rth := runtime.NewTestHarness().WithFakeKubeConfig(t)
defer rth.RemoveFakeKubeConfig(t)

config, err := clientcmd.BuildConfigFromFlags("", rth.FakeKubeConfigFileName())
assert.NoError(t, err)

client, err := BuildClients(config)
assert.NoError(t, err)

assert.NotNil(t, client.KubeClient)
assert.NotNil(t, client.SnapshotClient)
assert.NotNil(t, client.SmsCRClient)
}
Loading

0 comments on commit de656fd

Please sign in to comment.