Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"context"
"encoding/base64"
"fmt"
"maps"
"os"
"os/user"
"path/filepath"
Expand Down Expand Up @@ -32,6 +33,9 @@ type Client struct {
Project string
Log *log.Client
KubeconfigContext string

// defaultAnnotations is a map of default annotations to be set on all resources created or updated.
defaultAnnotations map[string]string
}

type ClientOpt func(c *Client) error
Expand All @@ -42,8 +46,9 @@ type ClientOpt func(c *Client) error
// * $HOME/.kube/config if exists
func New(ctx context.Context, apiClusterContext, project string, opts ...ClientOpt) (*Client, error) {
client := &Client{
Project: project,
KubeconfigContext: apiClusterContext,
Project: project,
KubeconfigContext: apiClusterContext,
defaultAnnotations: map[string]string{},
}
if err := client.loadConfig(apiClusterContext); err != nil {
return nil, err
Expand Down Expand Up @@ -101,6 +106,18 @@ func StaticToken(ctx context.Context) ClientOpt {
}
}

// DefaultAnnotations configures the client to set default annotations on all resources created or updated.
func DefaultAnnotations(k, v string) ClientOpt {
return func(c *Client) error {
if c.defaultAnnotations == nil {
c.defaultAnnotations = map[string]string{}
}

c.defaultAnnotations[k] = v
return nil
}
}

// NewScheme returns a *runtime.Scheme with all the relevant types registered.
func NewScheme() (*runtime.Scheme, error) {
scheme := runtime.NewScheme()
Expand Down Expand Up @@ -255,3 +272,17 @@ func ObjectName(obj runtimeclient.Object) types.NamespacedName {
func NamespacedName(name, project string) types.NamespacedName {
return types.NamespacedName{Name: name, Namespace: project}
}

// annotations returns a copy of the object's annotations with default annotations merged in.
func (c *Client) annotations(obj runtimeclient.Object) map[string]string {
annotations := obj.GetAnnotations()
if c.defaultAnnotations != nil {
if annotations == nil {
annotations = map[string]string{}
}

maps.Copy(annotations, c.defaultAnnotations)
}

return annotations
}
17 changes: 17 additions & 0 deletions api/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package api

import (
"context"

"sigs.k8s.io/controller-runtime/pkg/client"
)

// Create saves the object obj in the Kubernetes cluster.
// obj must be a struct pointer so that obj can be updated with the content returned by the Server.
func (c *Client) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error {
if c.defaultAnnotations != nil {
obj.SetAnnotations(c.annotations(obj))
}

return c.WithWatch.Create(ctx, obj, opts...)
}
108 changes: 108 additions & 0 deletions api/gitinfo/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package gitinfo

import (
"fmt"

apps "github.com/ninech/apis/apps/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
)

const (
PrivateKeySecretKey = "privatekey"
UsernameSecretKey = "username"
PasswordSecretKey = "password"
)

// Auth contains the credentials for a git repository.
type Auth struct {
Username *string
Password *string
SSHPrivateKey *string
}

func (a Auth) HasPrivateKey() bool {
return a.SSHPrivateKey != nil
}

func (a Auth) HasBasicAuth() bool {
return a.Username != nil && a.Password != nil
}

// NewAuthSecret returns a new secret for the given application. It can be used as
// a key for Get/Delete operations or as a base for populating credentials.
func NewAuthSecret(app *apps.Application) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: AuthSecretName(app),
Namespace: app.Namespace,
},
}
}

// ApplyToSecret writes the Auth credentials into the given secret's Data field.
// Only writes fields which are non-nil.
func (a Auth) ApplyToSecret(secret *corev1.Secret) {
if secret.Data == nil {
secret.Data = make(map[string][]byte)
}
if secret.Annotations == nil {
secret.Annotations = make(map[string]string)
}

if a.SSHPrivateKey != nil {
secret.Data[PrivateKeySecretKey] = []byte(*a.SSHPrivateKey)
}

if a.Username != nil {
secret.Data[UsernameSecretKey] = []byte(*a.Username)
}

if a.Password != nil {
secret.Data[PasswordSecretKey] = []byte(*a.Password)
}
}

// Enabled returns true if any kind of credentials are set in the GitAuth
func (a Auth) Enabled() bool {
return a.HasBasicAuth() || a.HasPrivateKey()
}

// Valid validates the credentials in the GitAuth
func (a Auth) Valid() error {
if a.SSHPrivateKey != nil {
if *a.SSHPrivateKey == "" {
return fmt.Errorf("the SSH private key cannot be empty")
}
}

if a.Username != nil && a.Password != nil {
if *a.Username == "" || *a.Password == "" {
return fmt.Errorf("the username/password cannot be empty")
}
}

return nil
}

// AuthSecretName returns the name of the secret which contains the git
// credentials for the given applications git source
func AuthSecretName(app *apps.Application) string {
return app.Name
}

// UpdateFromSecret updates the Auth object with the data from the given secret.
func (a *Auth) UpdateFromSecret(secret *corev1.Secret) {
if val, ok := secret.Data[PrivateKeySecretKey]; ok {
a.SSHPrivateKey = ptr.To(string(val))
}

if val, ok := secret.Data[UsernameSecretKey]; ok {
a.Username = ptr.To(string(val))
}

if val, ok := secret.Data[PasswordSecretKey]; ok {
a.Password = ptr.To(string(val))
}
}
28 changes: 15 additions & 13 deletions api/validation/git_information_client.go → api/gitinfo/client.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package validation
// Package gitinfo provides a client to interact with the git information service
// to retrieve metadata about git repositories.
package gitinfo

import (
"bytes"
Expand All @@ -12,22 +14,22 @@ import (
"time"

apps "github.com/ninech/apis/apps/v1alpha1"
"github.com/ninech/nctl/api/util"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/util/retry"
)

type GitInformationClient struct {
// Client is a client for the git information service.
type Client struct {
token string
url *url.URL
client *http.Client
logRetryFunc func(err error)
retryBackoff wait.Backoff
}

// NewGitInformationClient returns a client which can be used to retrieve
// New returns a client which can be used to retrieve
// metadata information about a given git repository
func NewGitInformationClient(address string, token string) (*GitInformationClient, error) {
func New(address string, token string) (*Client, error) {
u, err := url.Parse(address)
if err != nil {
return nil, fmt.Errorf("can not parse git information service URL: %w", err)
Expand All @@ -44,8 +46,8 @@ func setURLDefaults(u *url.URL) *url.URL {
return newURL
}

func defaultGitInformationClient(url *url.URL, token string) *GitInformationClient {
g := &GitInformationClient{
func defaultGitInformationClient(url *url.URL, token string) *Client {
g := &Client{
token: token,
url: url,
client: http.DefaultClient,
Expand All @@ -63,22 +65,22 @@ func defaultGitInformationClient(url *url.URL, token string) *GitInformationClie
return g
}

func (g *GitInformationClient) logError(format string, v ...any) {
func (g *Client) logError(format string, v ...any) {
fmt.Fprintf(os.Stderr, format, v...)
}

// SetLogRetryFunc allows to set the function which logs retries when doing
// requests to the git information service
func (g *GitInformationClient) SetLogRetryFunc(f func(err error)) {
func (g *Client) SetLogRetryFunc(f func(err error)) {
g.logRetryFunc = f
}

// SetRetryBackoffs sets the backoff properties for retries
func (g *GitInformationClient) SetRetryBackoffs(backoff wait.Backoff) {
func (g *Client) SetRetryBackoffs(backoff wait.Backoff) {
g.retryBackoff = backoff
}

func (g *GitInformationClient) repositoryInformation(ctx context.Context, git apps.GitTarget, auth util.GitAuth) (*apps.GitExploreResponse, error) {
func (g *Client) repositoryInformation(ctx context.Context, git apps.GitTarget, auth Auth) (*apps.GitExploreResponse, error) {
req := apps.GitExploreRequest{
Repository: git.URL,
Revision: git.Revision,
Expand All @@ -101,7 +103,7 @@ func (g *GitInformationClient) repositoryInformation(ctx context.Context, git ap
// RepositoryInformation returns information about a given git repository and
// optionally checks if a given revision can be found in the repo. It retries
// on client connection issues.
func (g *GitInformationClient) RepositoryInformation(ctx context.Context, git apps.GitTarget, auth util.GitAuth) (*apps.GitExploreResponse, error) {
func (g *Client) RepositoryInformation(ctx context.Context, git apps.GitTarget, auth Auth) (*apps.GitExploreResponse, error) {
var repoInfo *apps.GitExploreResponse
err := retry.OnError(
g.retryBackoff,
Expand All @@ -120,7 +122,7 @@ func (g *GitInformationClient) RepositoryInformation(ctx context.Context, git ap
return repoInfo, err
}

func (g *GitInformationClient) sendRequest(ctx context.Context, req apps.GitExploreRequest) (*apps.GitExploreResponse, error) {
func (g *Client) sendRequest(ctx context.Context, req apps.GitExploreRequest) (*apps.GitExploreResponse, error) {
data, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("can not JSON marshal request: %w", err)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package validation_test
package gitinfo_test

import (
"net/http"
"testing"
"time"

apps "github.com/ninech/apis/apps/v1alpha1"
"github.com/ninech/nctl/api/util"
"github.com/ninech/nctl/api/validation"
"github.com/ninech/nctl/api/gitinfo"
"github.com/ninech/nctl/internal/test"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/util/wait"
Expand All @@ -28,7 +27,7 @@ func TestRepositoryInformation(t *testing.T) {
for name, testCase := range map[string]struct {
git apps.GitTarget
token string
auth util.GitAuth
auth gitinfo.Auth
verifyRequest func(t *testing.T) func(p test.GitInfoServiceParsed, err error)
setResponse *test.GitInformationServiceResponse
expectedResponse *apps.GitExploreResponse
Expand All @@ -42,7 +41,7 @@ func TestRepositoryInformation(t *testing.T) {
Revision: "main",
},
token: "fake",
auth: util.GitAuth{
auth: gitinfo.Auth{
Username: ptr.To("fake"),
Password: ptr.To("fakePass"),
SSHPrivateKey: &dummyPrivateKey,
Expand Down Expand Up @@ -112,7 +111,7 @@ func TestRepositoryInformation(t *testing.T) {
gitInfo.SetResponse(*testCase.setResponse)
}

c, err := validation.NewGitInformationClient(gitInfo.URL(), testCase.token)
c, err := gitinfo.New(gitInfo.URL(), testCase.token)
is.NoError(err)

// we count the retries of the request
Expand Down
17 changes: 17 additions & 0 deletions api/update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package api

import (
"context"

"sigs.k8s.io/controller-runtime/pkg/client"
)

// Update updates the given obj in the Kubernetes cluster.
// obj must be a struct pointer so that obj can be updated with the content returned by the Server.
func (c *Client) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error {
if c.defaultAnnotations != nil {
obj.SetAnnotations(c.annotations(obj))
}

return c.WithWatch.Update(ctx, obj, opts...)
}
Loading