Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backend): implement create call #31

Merged
merged 9 commits into from
Oct 16, 2024
Merged
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"
agaudreault marked this conversation as resolved.
Show resolved Hide resolved
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) {
leoluz marked this conversation as resolved.
Show resolved Hide resolved
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) {
agaudreault marked this conversation as resolved.
Show resolved Hide resolved
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
Loading