Skip to content

Commit

Permalink
feat(backend): implement create call (#31)
Browse files Browse the repository at this point in the history
Signed-off-by: Alexandre Gaudreault <[email protected]>
  • Loading branch information
agaudreault authored Oct 16, 2024
1 parent 1f96499 commit 27d8826
Show file tree
Hide file tree
Showing 10 changed files with 500 additions and 60 deletions.
6 changes: 6 additions & 0 deletions api/argoproj/v1alpha1/groupversion_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ var (
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}

// ApplicationGroupVersionKind is group version kind used to identify an Application
ApplicationGroupVersionKind = schema.GroupVersionKind{Group: GroupVersion.Group, Version: GroupVersion.Version, Kind: "Application"}

// ApplicationGroupVersionKind is group version kind used to identify an AppProject
AppProjectGroupVersionKind = schema.GroupVersionKind{Group: GroupVersion.Group, Version: GroupVersion.Version, Kind: "AppProject"}

// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)
12 changes: 6 additions & 6 deletions api/ephemeral-access/v1alpha1/accessbinding_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,27 @@ import (
"reflect"
"testing"

"github.com/argoproj-labs/ephemeral-access/api/argoproj/v1alpha1"
argocd "github.com/argoproj-labs/ephemeral-access/api/argoproj/v1alpha1"
api "github.com/argoproj-labs/ephemeral-access/api/ephemeral-access/v1alpha1"
"github.com/argoproj-labs/ephemeral-access/test/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
)

func TestAccessBinding_RenderSubjects(t *testing.T) {
app, err := utils.ToUnstructured(&v1alpha1.Application{
ObjectMeta: v1.ObjectMeta{
app, err := utils.ToUnstructured(&argocd.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Annotations: map[string]string{
"test": "hello",
},
},
})
require.NoError(t, err)
project, err := utils.ToUnstructured(&v1alpha1.AppProject{
ObjectMeta: v1.ObjectMeta{
project, err := utils.ToUnstructured(&argocd.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Annotations: map[string]string{
"test": "world",
Expand Down
8 changes: 4 additions & 4 deletions cmd/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ import (
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"

appprojectv1alpha1 "github.com/argoproj-labs/ephemeral-access/api/argoproj/v1alpha1"
ephemeralaccessv1alpha1 "github.com/argoproj-labs/ephemeral-access/api/ephemeral-access/v1alpha1"
argocd "github.com/argoproj-labs/ephemeral-access/api/argoproj/v1alpha1"
api "github.com/argoproj-labs/ephemeral-access/api/ephemeral-access/v1alpha1"
"github.com/argoproj-labs/ephemeral-access/internal/controller"
"github.com/argoproj-labs/ephemeral-access/internal/controller/config"
"github.com/argoproj-labs/ephemeral-access/pkg/log"
Expand All @@ -48,8 +48,8 @@ var (

func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(ephemeralaccessv1alpha1.AddToScheme(scheme))
utilruntime.Must(appprojectv1alpha1.AddToScheme(scheme))
utilruntime.Must(api.AddToScheme(scheme))
utilruntime.Must(argocd.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}

Expand Down
69 changes: 53 additions & 16 deletions internal/backend/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ import (
"context"
"fmt"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"

argoprojv1alpha1 "github.com/argoproj-labs/ephemeral-access/api/argoproj/v1alpha1"
argocd "github.com/argoproj-labs/ephemeral-access/api/argoproj/v1alpha1"
api "github.com/argoproj-labs/ephemeral-access/api/ephemeral-access/v1alpha1"
"github.com/argoproj-labs/ephemeral-access/pkg/log"
)

const (
resourceType = "accessrequests"
managerName = "argocd-ephemeral-access-backend"

accessRequestUsernameField = "spec.subject.username"
accessRequestAppNameField = "spec.application.name"
Expand All @@ -31,13 +31,23 @@ const (
// layer (e.g. Kubernetes)
type Persister interface {

// CreateAccessRequest creates a new Access Request object
// CreateAccessRequest creates a new Access Request object and returns it
CreateAccessRequest(ctx context.Context, ar *api.AccessRequest) (*api.AccessRequest, error)
// ListAccessRequests returns all the AccessRequest matching the key criterias
ListAccessRequests(ctx context.Context, key *AccessRequestKey) (*api.AccessRequestList, error)

// ListAccessRequests returns all the AccessBindings matching the specified role and namespace
ListAccessBindings(ctx context.Context, roleName, namespace string) (*api.AccessBindingList, error)

// GetApplication returns an Unstructured object that represents the Application.
// An Unstructured object is returned to avoid importing the full object type or losing properties
// during unmarshalling from the partial typed object.
GetApplication(ctx context.Context, name, namespace string) (*unstructured.Unstructured, error)

// GetAppProject return an Unstructured object that represents the AppProject.
// An Unstructured object is returned to avoid importing the full object type or losing properties
// during unmarshalling from the partial typed object.
GetAppProject(ctx context.Context, name, namespace string) (*unstructured.Unstructured, error)
}

// K8sPersister is a K8s implementation for the Persister interface.
Expand All @@ -54,7 +64,7 @@ func NewK8sPersister(config *rest.Config, logger log.Logger) (*K8sPersister, err
return nil, fmt.Errorf("error adding ephemeralaccessv1alpha1 to k8s scheme: %w", err)
}

err = argoprojv1alpha1.AddToScheme(scheme.Scheme)
err = argocd.AddToScheme(scheme.Scheme)
if err != nil {
return nil, fmt.Errorf("error adding argoprojv1alpha1 to k8s scheme: %w", err)
}
Expand Down Expand Up @@ -128,7 +138,8 @@ func NewK8sPersister(config *rest.Config, logger log.Logger) (*K8sPersister, err
Scheme: scheme.Scheme,
Mapper: mapper,
Cache: &client.CacheOptions{
Reader: cache,
Reader: cache,
Unstructured: true,
},
}
k8sClient, err := client.New(config, clientOpts)
Expand Down Expand Up @@ -168,17 +179,15 @@ func (p *K8sPersister) StartCache(ctx context.Context) error {
}
}

// GetAccessRequestResource return a GroupVersionResource schema for the AccessRequest CRD.
func GetAccessRequestResource() schema.GroupVersionResource {
return schema.GroupVersionResource{
Group: api.GroupVersion.Group,
Version: api.GroupVersion.Version,
Resource: resourceType,
}
}

func (c *K8sPersister) CreateAccessRequest(ctx context.Context, ar *api.AccessRequest) (*api.AccessRequest, error) {
panic("unimplemented")
obj := ar.DeepCopy()
err := c.client.Create(ctx, obj, &client.CreateOptions{
FieldManager: managerName,
})
if err != nil {
return nil, fmt.Errorf("error creating access request: %w", err)
}
return obj, nil
}

func (c *K8sPersister) ListAccessRequests(ctx context.Context, key *AccessRequestKey) (*api.AccessRequestList, error) {
Expand Down Expand Up @@ -212,3 +221,31 @@ func (c *K8sPersister) ListAccessBindings(ctx context.Context, roleName, namespa
}
return list, nil
}

func (c *K8sPersister) GetApplication(ctx context.Context, name, namespace string) (*unstructured.Unstructured, error) {
obj := &unstructured.Unstructured{}
obj.SetGroupVersionKind(argocd.ApplicationGroupVersionKind)
key := client.ObjectKey{
Namespace: namespace,
Name: name,
}
err := c.client.Get(ctx, key, obj)
if err != nil {
return nil, fmt.Errorf("error retrieving application %s/%s from k8s: %w", namespace, name, err)
}
return obj, nil
}

func (c *K8sPersister) GetAppProject(ctx context.Context, name, namespace string) (*unstructured.Unstructured, error) {
obj := &unstructured.Unstructured{}
obj.SetGroupVersionKind(argocd.AppProjectGroupVersionKind)
key := client.ObjectKey{
Namespace: namespace,
Name: name,
}
err := c.client.Get(ctx, key, obj)
if err != nil {
return nil, fmt.Errorf("error retrieving appproject %s/%s from k8s: %w", namespace, name, err)
}
return obj, nil
}
153 changes: 153 additions & 0 deletions internal/backend/k8s_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ import (
"testing"
"time"

argocd "github.com/argoproj-labs/ephemeral-access/api/argoproj/v1alpha1"
"github.com/argoproj-labs/ephemeral-access/internal/backend"
"github.com/argoproj-labs/ephemeral-access/pkg/log"
"github.com/argoproj-labs/ephemeral-access/test/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
Expand Down Expand Up @@ -77,6 +81,47 @@ func TestK8sPersister(t *testing.T) {
require.NoError(t, err)
}()

t.Run("will create AccessRequest successfully", func(t *testing.T) {
// Given
nsName := "create-ar-success"
ns := utils.NewNamespace(nsName)
err = k8sClient.Create(ctx, ns)
require.NoError(t, err)

ar := utils.NewAccessRequestCreated()
ar.ObjectMeta.Namespace = nsName

// When
result, err := p.CreateAccessRequest(ctx, ar)

// Then
assert.NoError(t, err)
require.NotNil(t, result)
assert.NotEqual(t, ar, result)
assert.Equal(t, ar.GetName(), result.GetName())
assert.Equal(t, ar.GetNamespace(), result.GetNamespace())
})

t.Run("will return an error if create fails", func(t *testing.T) {
// Given
nsName := "create-ar-error"
ns := utils.NewNamespace(nsName)
err = k8sClient.Create(ctx, ns)
require.NoError(t, err)

ar := utils.NewAccessRequestCreated()
ar.ObjectMeta.Namespace = nsName
ar.ObjectMeta.Name = "--invalid--"

// When
result, err := p.CreateAccessRequest(ctx, ar)

// Then
assert.Error(t, err)
assert.ErrorContains(t, err, "metadata.name: Invalid value")
assert.Nil(t, result)
})

t.Run("will list AccessRequest successfully", func(t *testing.T) {
// Given
nsName := "list-ar-success"
Expand Down Expand Up @@ -324,4 +369,112 @@ func TestK8sPersister(t *testing.T) {
assert.Equal(t, 0, len(result.Items))
})

t.Run("will successfully get the Application", func(t *testing.T) {
// Given
nsName := "get-app"
name := "my-app"
destName := "dest-name-value"
ns := utils.NewNamespace(nsName)
err = k8sClient.Create(ctx, ns)
require.NoError(t, err)

app := &argocd.Application{
ObjectMeta: metav1.ObjectMeta{
Namespace: nsName,
Name: name,
},
Spec: argocd.ApplicationSpec{
Project: "test",
},
}
app.SetGroupVersionKind(argocd.ApplicationGroupVersionKind)
appU, err := utils.ToUnstructured(app)
require.NoError(t, err)
// spec.destination is required, but not defined in the ephemeral-access-spec
require.NoError(t, unstructured.SetNestedField(appU.Object, destName, "spec", "destination", "name"))
err = k8sClient.Create(ctx, appU)
require.NoError(t, err)

// When
result, err := p.GetApplication(ctx, name, nsName)

// Then
assert.NoError(t, err)
require.NotNil(t, result)
assert.Equal(t, name, result.GetName())
assert.Equal(t, nsName, result.GetNamespace())
gotDestName, ok, err := unstructured.NestedString(result.Object, "spec", "destination", "name")
assert.NoError(t, err)
assert.True(t, ok)
assert.Equal(t, destName, gotDestName)
})

t.Run("will return an error if Application does not exist", func(t *testing.T) {
// Given
nsName := "get-app-notfound"
ns := utils.NewNamespace(nsName)
err = k8sClient.Create(ctx, ns)
require.NoError(t, err)

// When
result, err := p.GetApplication(ctx, "not-found", nsName)

// Then
assert.Error(t, err)
assert.True(t, apierrors.IsNotFound(err))
assert.Nil(t, result)
})

t.Run("will successfully get the AppProject", func(t *testing.T) {
// Given
nsName := "get-project"
name := "my-project"
ns := utils.NewNamespace(nsName)
err = k8sClient.Create(ctx, ns)
require.NoError(t, err)

project := &argocd.AppProject{
ObjectMeta: metav1.ObjectMeta{
Namespace: nsName,
Name: name,
},
Spec: argocd.AppProjectSpec{
Roles: []argocd.ProjectRole{
{Name: "test"},
},
},
}
err = k8sClient.Create(ctx, project)
require.NoError(t, err)

// When
result, err := p.GetAppProject(ctx, name, nsName)

// Then
assert.NoError(t, err)
require.NotNil(t, result)
assert.Equal(t, name, result.GetName())
assert.Equal(t, nsName, result.GetNamespace())
roles, ok, err := unstructured.NestedSlice(result.Object, "spec", "roles")
assert.NoError(t, err)
assert.True(t, ok)
assert.Equal(t, len(project.Spec.Roles), len(roles))
})

t.Run("will return an error if AppProject does not exist", func(t *testing.T) {
// Given
nsName := "get-project-notfound"
ns := utils.NewNamespace(nsName)
err = k8sClient.Create(ctx, ns)
require.NoError(t, err)

// When
result, err := p.GetAppProject(ctx, "not-found", nsName)

// Then
assert.Error(t, err)
assert.True(t, apierrors.IsNotFound(err))
assert.Nil(t, result)
})

}
Loading

0 comments on commit 27d8826

Please sign in to comment.