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: user-identity-mapper job #24

Merged
merged 3 commits into from
Mar 25, 2024
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
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() {

Check warning on line 27 in cmd/user-identity-mapper/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/user-identity-mapper/main.go#L27

Added line #L27 was not covered by tests
// 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 {

Check warning on line 31 in cmd/user-identity-mapper/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/user-identity-mapper/main.go#L29-L31

Added lines #L29 - L31 were not covered by tests

logger := log.New(cmd.OutOrStderr())

Check warning on line 33 in cmd/user-identity-mapper/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/user-identity-mapper/main.go#L33

Added line #L33 was not covered by tests
// 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)

Check warning on line 38 in cmd/user-identity-mapper/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/user-identity-mapper/main.go#L35-L38

Added lines #L35 - L38 were not covered by tests
}

// 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)

Check warning on line 45 in cmd/user-identity-mapper/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/user-identity-mapper/main.go#L42-L45

Added lines #L42 - L45 were not covered by tests
}
cl, err := runtimeclient.New(cfg, runtimeclient.Options{
Scheme: scheme,
})
if err != nil {
logger.Error("unable to create a client", "error", err)
os.Exit(1)

Check warning on line 52 in cmd/user-identity-mapper/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/user-identity-mapper/main.go#L47-L52

Added lines #L47 - L52 were not covered by tests
}
return CreateUserIdentityMappings(cmd.Context(), logger, cl)

Check warning on line 54 in cmd/user-identity-mapper/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/user-identity-mapper/main.go#L54

Added line #L54 was not covered by tests
},
}

if err := cmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)

Check warning on line 60 in cmd/user-identity-mapper/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/user-identity-mapper/main.go#L58-L60

Added lines #L58 - L60 were not covered by tests
}
}
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
Loading