Skip to content

Commit

Permalink
feat: user-identity-mapper job (#24)
Browse files Browse the repository at this point in the history
code coming from https://github.com/codeready-toolchain/sandbox-sre/blob/master/cmd/user-identity-mapper/

Signed-off-by: Xavier Coulon <[email protected]>
Co-authored-by: Matous Jobanek <[email protected]>
  • Loading branch information
xcoulon and MatousJobanek authored Mar 25, 2024
1 parent a7e9be9 commit 2705510
Show file tree
Hide file tree
Showing 6 changed files with 380 additions and 5 deletions.
32 changes: 32 additions & 0 deletions cmd/user-identity-mapper/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
################################################################################################
# Builder image
# See https://hub.docker.com/_/golang/
################################################################################################
FROM golang:1.20 as builder

ARG OS=linux
ARG ARCH=amd64

WORKDIR /usr/src/app

# pre-copy/cache parent go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change
COPY go.mod go.sum ./
RUN go mod download && go mod verify

COPY pkg ./pkg
COPY cmd/user-identity-mapper ./cmd/user-identity-mapper

RUN go build -v -o user-identity-mapper cmd/user-identity-mapper/*.go

################################################################################################
# user-identity-mapper image to be run by the job on OpenShift
################################################################################################
FROM registry.access.redhat.com/ubi9/ubi-minimal:latest as user-identity-mapper

# Copy the generated binary into the $PATH so it can be invoked
COPY --from=builder /usr/src/app/user-identity-mapper /usr/local/bin/

# Run as non-root user
USER 1001

CMD ["/usr/local/bin/user-identity-mapper"]
62 changes: 62 additions & 0 deletions cmd/user-identity-mapper/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// 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 (
"fmt"
"os"

"github.com/charmbracelet/log"
userv1 "github.com/openshift/api/user/v1"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/runtime"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/config"
)

func main() {
// cmd the command that maps an identity to its parent user
cmd := &cobra.Command{
Use: "user-identity-mapper",
RunE: func(cmd *cobra.Command, args []string) error {

logger := log.New(cmd.OutOrStderr())
// Get a config to talk to the apiserver
cfg, err := config.GetConfig()
if err != nil {
logger.Error("unable to load config", "error", err)
os.Exit(1)
}

// create client that will be used for retrieving the host operator secret & ToolchainCluster CRs
scheme := runtime.NewScheme()
if err := userv1.Install(scheme); err != nil {
logger.Error("unable to install scheme", "error", err)
os.Exit(1)
}
cl, err := runtimeclient.New(cfg, runtimeclient.Options{
Scheme: scheme,
})
if err != nil {
logger.Error("unable to create a client", "error", err)
os.Exit(1)
}
return CreateUserIdentityMappings(cmd.Context(), logger, cl)
},
}

if err := cmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
54 changes: 54 additions & 0 deletions cmd/user-identity-mapper/user_identity_mapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package main

import (
"context"
"fmt"

"github.com/charmbracelet/log"
userv1 "github.com/openshift/api/user/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
)

func CreateUserIdentityMappings(ctx context.Context, logger *log.Logger, cl runtimeclient.Client) error {
logger.Info("listing users...")
users := &userv1.UserList{}
if err := cl.List(ctx, users, runtimeclient.MatchingLabels{
"provider": "sandbox-sre",
}); err != nil {
return fmt.Errorf("unable to list users: %w", err)
}
for _, user := range users.Items {
logger.Info("listing identities", "username", user.Name)
identities := userv1.IdentityList{}
if err := cl.List(ctx, &identities, runtimeclient.MatchingLabels{
"provider": "sandbox-sre",
"username": user.Name,
}); err != nil {
return fmt.Errorf("unable to list identities: %w", err)
}
if len(identities.Items) == 0 {
logger.Errorf("no identity associated with user %q", user.Name)
continue
}
for _, identity := range identities.Items {
logger.Info("creating/updating identity mapping", "user", user.Name, "identity", identity.Name)
if err := cl.Create(ctx, &userv1.UserIdentityMapping{
ObjectMeta: metav1.ObjectMeta{
Name: identity.Name,
},
User: corev1.ObjectReference{
Name: user.Name,
},
Identity: corev1.ObjectReference{
Name: identity.Name,
},
}); err != nil && !errors.IsAlreadyExists(err) {
return fmt.Errorf("unable to create identity mapping for username %q and identity %q: %w", user.Name, identity.Name, err)
}
}
}
return nil
}
189 changes: 189 additions & 0 deletions cmd/user-identity-mapper/user_identity_mapper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package main_test

import (
"bytes"
"context"
"fmt"
"testing"

"github.com/codeready-toolchain/toolchain-common/pkg/test"
useridentitymapper "github.com/kubesaw/ksctl/cmd/user-identity-mapper"

"github.com/charmbracelet/log"
userv1 "github.com/openshift/api/user/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
)

func TestUserIdentityMapper(t *testing.T) {

// given
s := scheme.Scheme
err := userv1.Install(s)
require.NoError(t, err)
user1 := &userv1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "user1",
Labels: map[string]string{
"provider": "sandbox-sre",
},
},
}
identity1 := &userv1.Identity{
ObjectMeta: metav1.ObjectMeta{
Name: "identity1",
Labels: map[string]string{
"provider": "sandbox-sre",
"username": "user1",
},
},
}
user2 := &userv1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "user2",
Labels: map[string]string{
"provider": "sandbox-sre",
},
},
}
identity2 := &userv1.Identity{
ObjectMeta: metav1.ObjectMeta{
Name: "identity2",
Labels: map[string]string{
"provider": "sandbox-sre",
"username": "user2",
},
},
}
user3 := &userv1.User{
ObjectMeta: metav1.ObjectMeta{
Name: "user3",
// not managed by sandbox-sre
},
}
identity3 := &userv1.Identity{
ObjectMeta: metav1.ObjectMeta{
Name: "identity3",
Labels: map[string]string{
"provider": "sandbox-sre",
"username": "user3",
},
},
}

t.Run("success", func(t *testing.T) {
// given
out := new(bytes.Buffer)
logger := log.New(out)
cl := test.NewFakeClient(t, user1, identity1, user2, identity2, user3, identity3)

// when
err := useridentitymapper.CreateUserIdentityMappings(context.TODO(), logger, cl)

// then
require.NoError(t, err)
assert.NotContains(t, out.String(), "unable to list identities")
uim := &userv1.UserIdentityMapping{}
// `user1` and `user2` are not managed by sandbox (ie, labelled with `provider: sandbox-sre`), hence the `UserIdentityMappings` exist
require.NoError(t, cl.Get(context.TODO(), types.NamespacedName{Name: identity1.Name}, uim))
assert.Equal(t, identity1.Name, uim.Identity.Name)
assert.Equal(t, user1.Name, uim.User.Name)
require.NoError(t, cl.Get(context.TODO(), types.NamespacedName{Name: identity2.Name}, uim))
assert.Equal(t, identity2.Name, uim.Identity.Name)
assert.Equal(t, user2.Name, uim.User.Name)
})

t.Run("failures", func(t *testing.T) {

t.Run("user and identities not labelled", func(t *testing.T) {
// given
out := new(bytes.Buffer)
logger := log.New(out)
cl := test.NewFakeClient(t, user3, identity3)

// when
err := useridentitymapper.CreateUserIdentityMappings(context.TODO(), logger, cl)

// then
require.NoError(t, err)
assert.NotContains(t, out.String(), "unable to list identities")
// `user3` is not managed by sandbox (ie, not labelled with `provider: sandbox-sre`), , hence the `UserIdentityMappings` does not exist
require.EqualError(t, cl.Get(context.TODO(), types.NamespacedName{Name: identity3.Name}, &userv1.UserIdentityMapping{}), `useridentitymappings.user.openshift.io "identity3" not found`)
})

t.Run("missing identity", func(t *testing.T) {
// given
out := new(bytes.Buffer)
logger := log.New(out)
cl := test.NewFakeClient(t, user1)

// when
err := useridentitymapper.CreateUserIdentityMappings(context.TODO(), logger, cl)

// then
require.NoError(t, err)
assert.Contains(t, out.String(), `no identity associated with user "user1"`)
require.EqualError(t, cl.Get(context.TODO(), types.NamespacedName{Name: identity1.Name}, &userv1.UserIdentityMapping{}), `useridentitymappings.user.openshift.io "identity1" not found`)
})

t.Run("cannot list users", func(t *testing.T) {
// given
out := new(bytes.Buffer)
logger := log.New(out)
cl := test.NewFakeClient(t, user1, identity1)
cl.MockList = func(ctx context.Context, list runtimeclient.ObjectList, opts ...runtimeclient.ListOption) error {
if _, ok := list.(*userv1.UserList); ok {
return fmt.Errorf("mock error")
}
return cl.Client.List(ctx, list, opts...)
}
// when
err := useridentitymapper.CreateUserIdentityMappings(context.TODO(), logger, cl)

// then
assert.EqualError(t, err, "unable to list users: mock error")
})

t.Run("cannot list identities", func(t *testing.T) {
// given
out := new(bytes.Buffer)
logger := log.New(out)
cl := test.NewFakeClient(t, user1, identity1, user2, identity2)
cl.MockList = func(ctx context.Context, list runtimeclient.ObjectList, opts ...runtimeclient.ListOption) error {
if _, ok := list.(*userv1.IdentityList); ok {
return fmt.Errorf("mock error")
}
return cl.Client.List(ctx, list, opts...)
}

// when
err := useridentitymapper.CreateUserIdentityMappings(context.TODO(), logger, cl)

// then
assert.EqualError(t, err, "unable to list identities: mock error")
})

t.Run("cannot create user-identity mapping", func(t *testing.T) {
// given
out := new(bytes.Buffer)
logger := log.New(out)
cl := test.NewFakeClient(t, user1, identity1, user2, identity2)
cl.MockCreate = func(ctx context.Context, obj runtimeclient.Object, opts ...runtimeclient.CreateOption) error {
if _, ok := obj.(*userv1.UserIdentityMapping); ok {
return fmt.Errorf("mock error")
}
return cl.Client.Create(ctx, obj, opts...)
}

// when
err := useridentitymapper.CreateUserIdentityMappings(context.TODO(), logger, cl)

// then
assert.EqualError(t, err, `unable to create identity mapping for username "user1" and identity "identity1": mock error`)
})
})
}
Loading

0 comments on commit 2705510

Please sign in to comment.