Files
teleport/operator/controllers/resources/user_controller_test.go
T
29e46a2a6a buddy: Fix incorrect use of loop variables (#16306)
* Fix incorrect use of loop variables

This commit fixes a few occurrences of loop variables being
incorrectly used in the context of Go-routines or (most frequently)
parallel tests. To fix the issues, we create a local copy of the range
variables before the parallel tests (or Go-routine), as suggested in
the documentation of the `testing` package:

https://pkg.go.dev/testing#hdr-Subtests_and_Sub_benchmarks

Issues were found using the `loopvarcapture` linter.

Signed-off-by: Roman Tkachenko <roman@goteleport.com>

* fix TestTraceProvider/spans_exported_with_gRPC+TLS

* run TestSSH serially

* operator: Conserve 'created_by' data in user spec

Signed-off-by: Roman Tkachenko <roman@goteleport.com>
Co-authored-by: Renato Costa <renato@cockroachlabs.com>
Co-authored-by: Tim Ross <tim.ross@goteleport.com>
Co-authored-by: Hugo Hervieux <hugo.hervieux@goteleport.com>
2022-09-14 14:31:56 +00:00

459 lines
13 KiB
Go

/*
Copyright 2022 Gravitational, Inc.
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 resources
import (
"context"
"encoding/json"
"testing"
"github.com/gravitational/trace"
"github.com/mitchellh/mapstructure"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/yaml"
kclient "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/gravitational/teleport/api/types"
resourcesv2 "github.com/gravitational/teleport/operator/apis/resources/v2"
resourcesv5 "github.com/gravitational/teleport/operator/apis/resources/v5"
)
const teleportUserKind = "TeleportUser"
var teleportUserGVK = schema.GroupVersionKind{
Group: resourcesv2.GroupVersion.Group,
Version: resourcesv2.GroupVersion.Version,
Kind: teleportUserKind,
}
func TestUserCreation(t *testing.T) {
ctx := context.Background()
setup := setupTestEnv(t)
userName := validRandomResourceName("user-")
teleportCreateDummyRole(ctx, t, "a", setup.tClient)
teleportCreateDummyRole(ctx, t, "b", setup.tClient)
// The user is created in K8S
k8sCreateDummyUser(ctx, t, setup.k8sClient, setup.namespace.Name, userName)
fastEventually(t, func() bool {
tUser, err := setup.tClient.GetUser(userName, false)
if trace.IsNotFound(err) {
return false
}
require.NoError(t, err)
require.Equal(t, tUser.GetName(), userName)
require.Contains(t, tUser.GetMetadata().Labels, types.OriginLabel)
require.Equal(t, tUser.GetMetadata().Labels[types.OriginLabel], types.OriginKubernetes)
return true
})
// The user is deleted in K8S
k8sDeleteUser(ctx, t, setup.k8sClient, userName, setup.namespace.Name)
fastEventually(t, func() bool {
_, err := setup.tClient.GetUser(userName, false)
return trace.IsNotFound(err)
})
}
func TestUserCreationFromYAML(t *testing.T) {
ctx := context.Background()
setup := setupTestEnv(t)
teleportCreateDummyRole(ctx, t, "a", setup.tClient)
tests := []struct {
name string
userSpecYAML string
shouldFail bool
expectedSpec *types.UserSpecV2
}{
{
name: "Valid user without traits",
userSpecYAML: `
roles:
- a
`,
shouldFail: false,
expectedSpec: &types.UserSpecV2{
Roles: []string{"a"},
},
},
{
name: "Valid user with trait (list with single element)",
userSpecYAML: `
roles:
- a
traits:
'foo': ['bar']
`,
shouldFail: false,
expectedSpec: &types.UserSpecV2{
Roles: []string{"a"},
Traits: map[string][]string{
"foo": {"bar"},
},
},
},
{
name: "Valid user with traits (list with multiple element)",
userSpecYAML: `
roles:
- a
traits:
'foo': ['bar', 'baz']
`,
shouldFail: false,
expectedSpec: &types.UserSpecV2{
Roles: []string{"a"},
Traits: map[string][]string{
"foo": {"bar", "baz"},
},
},
},
{
name: "Invalid user with non-existing role",
userSpecYAML: `
roles:
- does-not-exist
traits:
'foo': ['bar', 'baz']
`,
shouldFail: true,
expectedSpec: nil,
},
}
for _, tc := range tests {
tc := tc // capture range variable
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Creating the Kubernetes resource. We are using an untyped client to be able to create invalid resources.
userManifest := map[string]interface{}{}
err := yaml.Unmarshal([]byte(tc.userSpecYAML), &userManifest)
require.NoError(t, err)
userName := validRandomResourceName("user-")
obj := getUnstructuredObjectFromGVK(teleportUserGVK)
obj.Object["spec"] = userManifest
obj.SetName(userName)
obj.SetNamespace(setup.namespace.Name)
err = setup.k8sClient.Create(ctx, obj)
require.NoError(t, err)
// If failure is expected we should not see the resource in Teleport
if tc.shouldFail {
fastEventually(t, func() bool {
// We check status.Conditions was updated, this means the reconciliation happened
_ = setup.k8sClient.Get(ctx, kclient.ObjectKey{
Namespace: setup.namespace.Name,
Name: userName,
}, obj)
errorConditions := getUserStatusConditionError(obj.Object)
// If there's no error condition, reconciliation has not happened yet
if len(errorConditions) == 0 {
return false
}
_, err := setup.tClient.GetUser(userName, false /* withSecrets */)
require.True(t, trace.IsNotFound(err), "The user should not be created in Teleport")
return true
})
} else {
// We wait for Teleport resource creation
fastEventually(t, func() bool {
tUser, err := setup.tClient.GetUser(userName, false /* withSecrets */)
// If the resource creation should succeed we check the resource was found and validate ownership labels
if trace.IsNotFound(err) {
return false
}
require.NoError(t, err)
require.Equal(t, tUser.GetName(), userName)
require.Contains(t, tUser.GetMetadata().Labels, types.OriginLabel)
require.Equal(t, tUser.GetMetadata().Labels[types.OriginLabel], types.OriginKubernetes)
require.Equal(t, setup.operatorName, tUser.GetCreatedBy().User.Name)
expectedUser := &types.UserV2{
Metadata: types.Metadata{},
Spec: *tc.expectedSpec,
}
_ = expectedUser.CheckAndSetDefaults()
compareUserSpecs(t, expectedUser, tUser)
return true
})
}
// Teardown
// The role is deleted in K8S
k8sDeleteUser(ctx, t, setup.k8sClient, userName, setup.namespace.Name)
// We wait for the role deletion in Teleport
fastEventually(t, func() bool {
_, err := setup.tClient.GetUser(userName, false /* withSecrets */)
return trace.IsNotFound(err)
})
})
}
}
func compareUserSpecs(t *testing.T, expectedUser, actualUser types.User) {
expectedJSON, _ := json.Marshal(expectedUser)
expected := make(map[string]interface{})
_ = json.Unmarshal(expectedJSON, &expected)
actualJSON, _ := json.Marshal(actualUser)
actual := make(map[string]interface{})
_ = json.Unmarshal(actualJSON, &actual)
// We don't want compare spec.created_by and metadata as they were tested before and are not 100%
// managed by the operator
delete(expected["spec"].(map[string]interface{}), "created_by")
delete(actual["spec"].(map[string]interface{}), "created_by")
require.Equal(t, expected["spec"], actual["spec"])
}
// TestUserDeletionDrift tests how the Kubernetes operator reacts when it is asked to delete a user that was
// already deleted in Teleport
func TestUserDeletionDrift(t *testing.T) {
// Setup section: start the operator, and create a user
ctx := context.Background()
setup := setupTestEnv(t)
userName := validRandomResourceName("user-")
teleportCreateDummyRole(ctx, t, "a", setup.tClient)
teleportCreateDummyRole(ctx, t, "b", setup.tClient)
// The user is created in K8S
k8sCreateDummyUser(ctx, t, setup.k8sClient, setup.namespace.Name, userName)
fastEventually(t, func() bool {
tUser, err := setup.tClient.GetUser(userName, false)
if trace.IsNotFound(err) {
return false
}
require.NoError(t, err)
require.Equal(t, tUser.GetName(), userName)
require.Contains(t, tUser.GetMetadata().Labels, types.OriginLabel)
require.Equal(t, tUser.GetMetadata().Labels[types.OriginLabel], types.OriginKubernetes)
return true
})
// We cause a drift by altering the Teleport resource.
// To make sure the operator does not reconcile while we're finished we suspend the operator
setup.stopKubernetesOperator()
err := setup.tClient.DeleteUser(ctx, userName)
require.NoError(t, err)
fastEventually(t, func() bool {
_, err := setup.tClient.GetUser(userName, false)
return trace.IsNotFound(err)
})
// We flag the role for deletion in Kubernetes (it won't be fully remopved until the operator has processed it and removed the finalizer)
k8sDeleteUser(ctx, t, setup.k8sClient, userName, setup.namespace.Name)
// Test section: We resume the operator, it should reconcile and recover from the drift
setup.startKubernetesOperator(t)
// The operator should handle the failed Teleport deletion gracefully and unlock the Kubernetes resource deletion
var k8sUser resourcesv2.TeleportUser
fastEventually(t, func() bool {
err = setup.k8sClient.Get(ctx, kclient.ObjectKey{
Namespace: setup.namespace.Name,
Name: userName,
}, &k8sUser)
return kerrors.IsNotFound(err)
})
}
func TestUserUpdate(t *testing.T) {
ctx := context.Background()
setup := setupTestEnv(t)
teleportCreateDummyRole(ctx, t, "a", setup.tClient)
teleportCreateDummyRole(ctx, t, "b", setup.tClient)
teleportCreateDummyRole(ctx, t, "x", setup.tClient)
teleportCreateDummyRole(ctx, t, "y", setup.tClient)
teleportCreateDummyRole(ctx, t, "z", setup.tClient)
userName := validRandomResourceName("user-")
// The user does not exist in K8S
var r resourcesv2.TeleportUser
err := setup.k8sClient.Get(ctx, kclient.ObjectKey{
Namespace: setup.namespace.Name,
Name: userName,
}, &r)
require.True(t, kerrors.IsNotFound(err))
// The user is created in Teleport
tUser, err := types.NewUser(userName)
require.NoError(t, err)
tUser.SetRoles([]string{"a", "b"})
metadata := tUser.GetMetadata()
metadata.Labels = map[string]string{types.OriginLabel: types.OriginKubernetes}
tUser.SetMetadata(metadata)
err = setup.tClient.CreateUser(ctx, tUser)
require.NoError(t, err)
// The user is created in K8S
k8sUser := resourcesv2.TeleportUser{
ObjectMeta: metav1.ObjectMeta{
Name: userName,
Namespace: setup.namespace.Name,
},
Spec: resourcesv2.TeleportUserSpec{
Roles: []string{"x", "z"},
},
}
k8sCreateUser(ctx, t, setup.k8sClient, &k8sUser)
// The user is updated in Teleport
fastEventually(t, func() bool {
tUser, err := setup.tClient.GetUser(userName, false)
require.NoError(t, err)
// TeleportUser was updated with new roles
return assert.ElementsMatch(t, tUser.GetRoles(), []string{"x", "z"})
})
// Updating the user in K8S
var k8sUserNewVersion resourcesv2.TeleportUser
err = setup.k8sClient.Get(ctx, kclient.ObjectKey{
Namespace: setup.namespace.Name,
Name: userName,
}, &k8sUserNewVersion)
require.NoError(t, err)
k8sUserNewVersion.Spec.Roles = append(k8sUserNewVersion.Spec.Roles, "y")
err = setup.k8sClient.Update(ctx, &k8sUserNewVersion)
require.NoError(t, err)
// Updates the user in Teleport
fastEventually(t, func() bool {
tUser, err := setup.tClient.GetUser(userName, false)
require.NoError(t, err)
// TeleportUser updated with new roles
return assert.ElementsMatch(t, tUser.GetRoles(), []string{"x", "z", "y"})
})
}
func k8sCreateDummyUser(ctx context.Context, t *testing.T, kc kclient.Client, namespace, userName string) {
user := resourcesv2.TeleportUser{
ObjectMeta: metav1.ObjectMeta{
Name: userName,
Namespace: namespace,
},
Spec: resourcesv2.TeleportUserSpec{
Roles: []string{"a", "b"},
},
}
k8sCreateUser(ctx, t, kc, &user)
}
func k8sDeleteUser(ctx context.Context, t *testing.T, kc kclient.Client, userName, namespace string) {
user := resourcesv2.TeleportUser{
ObjectMeta: metav1.ObjectMeta{
Name: userName,
Namespace: namespace,
},
}
err := kc.Delete(ctx, &user)
require.NoError(t, err)
}
func k8sCreateUser(ctx context.Context, t *testing.T, kc kclient.Client, user *resourcesv2.TeleportUser) {
err := kc.Create(ctx, user)
require.NoError(t, err)
}
func TestAddTeleportResourceOriginUser(t *testing.T) {
r := UserReconciler{}
tests := []struct {
name string
resource types.User
}{
{
name: "origin already set correctly",
resource: &types.UserV2{
Metadata: types.Metadata{
Name: "user with correct origin",
Labels: map[string]string{types.OriginLabel: types.OriginKubernetes},
},
},
},
{
name: "origin already set incorrectly",
resource: &types.UserV2{
Metadata: types.Metadata{
Name: "user with correct origin",
Labels: map[string]string{types.OriginLabel: types.OriginConfigFile},
},
},
},
{
name: "origin not set",
resource: &types.UserV2{
Metadata: types.Metadata{
Name: "user with correct origin",
Labels: map[string]string{"foo": "bar"},
},
},
},
{
name: "no labels",
resource: &types.UserV2{
Metadata: types.Metadata{
Name: "user with no labels",
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
r.addTeleportResourceOrigin(tc.resource)
metadata := tc.resource.GetMetadata()
require.Contains(t, metadata.Labels, types.OriginLabel)
require.Equal(t, metadata.Labels[types.OriginLabel], types.OriginKubernetes)
})
}
}
func getUserStatusConditionError(object map[string]interface{}) []metav1.Condition {
var conditionsWithError []metav1.Condition
var status resourcesv5.TeleportRoleStatus
_ = mapstructure.Decode(object["status"], &status)
for _, condition := range status.Conditions {
if condition.Status == metav1.ConditionFalse {
conditionsWithError = append(conditionsWithError, condition)
}
}
return conditionsWithError
}