Skip to content
This repository has been archived by the owner on Mar 11, 2021. It is now read-only.

Use namespaces from tenant for Deployments API #2110

Merged
merged 4 commits into from
Jun 15, 2018
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
35 changes: 34 additions & 1 deletion controller/deployments_urlprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ func newTenantURLProviderFromTenant(t *app.UserService, token string, proxyURL s
namespaceMap := make(map[string]*app.NamespaceAttributes)
for i, namespace := range t.Attributes.Namespaces {
namespaceMap[*namespace.Name] = t.Attributes.Namespaces[i]
if *namespace.Type == "user" {
if namespace.Type != nil && *namespace.Type == "user" {
defaultNamespace = namespace
}
}
Expand All @@ -185,6 +185,39 @@ func NewTenantURLProviderFromTenant(t *app.UserService, token string, proxyURL s
return newTenantURLProviderFromTenant(t, token, proxyURL)
}

// GetEnvironmentMapping returns a map whose keys are environment names, and values are the Kubernetes namespaces
// that represent those environments
func (up *tenantURLProvider) GetEnvironmentMapping() map[string]string {
result := make(map[string]string)
// Exclude internal namespaces where the user cannot deploy applications

// Deployments API will receive requests by environment name (e.g. "run", "stage").
// These names correspond to the "type" attribute in Namespaces.
for envNS, attr := range up.namespaces {
envName := attr.Type
if envName == nil || len(*envName) == 0 {
log.Error(nil, map[string]interface{}{
"namespace": envNS,
}, "namespace has no type")
} else if !isInternalNamespace(*envName) {
result[*envName] = envNS
}
}
return result
}

// Types of namespaces where the user does not deploy applications
var internalNamespaceTypes = []string{"user", "che", "jenkins"}

func isInternalNamespace(envType string) bool {
for _, internalType := range internalNamespaceTypes {
if envType == internalType {
return true
}
}
return false
}

func (up *tenantURLProvider) GetAPIToken() (*string, error) {
return &up.apiToken, nil
}
Expand Down
44 changes: 44 additions & 0 deletions controller/deployments_urlprovider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,50 @@ func TestTenantGetUnknownMetricsToken(t *testing.T) {
require.Nil(t, mtoken)
}

func TestTenantGetEnvironmentMapping(t *testing.T) {
testCases := []struct {
testName string
inputFile string
expectedMap map[string]string
}{
{
testName: "Basic",
inputFile: "user-services.json",
expectedMap: map[string]string{
"run": "theuser-run",
"stage": "theuser-stage",
},
},
{
testName: "No Type",
inputFile: "user-services-no-type.json",
expectedMap: map[string]string{
"run": "theuser-run",
},
},
{
testName: "Empty Type",
inputFile: "user-services-empty-type.json",
expectedMap: map[string]string{
"run": "theuser-run",
},
},
}

for _, testCase := range testCases {
t.Run(testCase.testName, func(t *testing.T) {
userSvc, err := getTenantFromFile(testCase.inputFile)
require.NoError(t, err, "error reading tenant")
provider, err := controller.NewTenantURLProviderFromTenant(userSvc, defaultAPIToken, "")
require.NoError(t, err, "error creating URL provider")

envMap := provider.GetEnvironmentMapping()
require.NotNil(t, envMap)
require.Equal(t, testCase.expectedMap, envMap, "GetEnvironmentMapping() did not return the expected environments")
})
}
}

//////////////////////////////////////////////////////////////////////////////////////////////////

func tostring(item interface{}) string {
Expand Down
48 changes: 2 additions & 46 deletions kubernetes/deployments_kubeclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ type route struct {
// This hasn't been done, because the rest of fabric8 seems to assume the cluster is the same.
// For most uses, the proxy server will hide this issue - but not for metrics/logging and console.
type BaseURLProvider interface {
GetEnvironmentMapping() map[string]string
GetAPIURL() (*string, error)
GetMetricsURL(envNS string) (*string, error)
GetConsoleURL(envNS string) (*string, error)
Expand Down Expand Up @@ -177,12 +178,8 @@ func NewKubeClient(config *KubeClientConfig) (KubeClientInterface, error) {
if config.MetricsGetter == nil {
config.MetricsGetter = &defaultGetter{}
}
// Get environments from config map
envMap, err := getEnvironmentsFromConfigMap(kubeAPI, config.UserNamespace)
if err != nil {
return nil, err
}

envMap := config.GetEnvironmentMapping()
kubeClient := &kubeClient{
config: config,
envMap: envMap,
Expand Down Expand Up @@ -756,47 +753,6 @@ func (oc *openShiftAPIClient) GetBuildConfigs(namespace string, labelSelector st
return oc.getResource(bcURL, false)
}

func getEnvironmentsFromConfigMap(kube KubeRESTAPI, userNamespace string) (map[string]string, error) {
// fabric8 creates a ConfigMap in the user namespace with information on environments
const envConfigMap string = "fabric8-environments"
const providerLabel string = "fabric8"
configmap, err := kube.ConfigMaps(userNamespace).Get(envConfigMap, metaV1.GetOptions{})
if err != nil {
log.Error(nil, map[string]interface{}{
"err": err,
"user_namespace": userNamespace,
}, "failed to get environment list from %s config map", envConfigMap)
return nil, convertError(errs.WithStack(err), "failed to get environment list")
}
// Check that config map has the expected label
if configmap.Labels["provider"] != providerLabel {
return nil, errs.Errorf("unknown or missing provider %s for environments config map in namespace %s", providerLabel, userNamespace)
}
// Parse config map data to construct environments map
envMap := make(map[string]string)
const namespaceProp string = "namespace"
// Config map keys are environment names
for key, value := range configmap.Data {
// Look through value for namespace property
var namespace string
lines := strings.Split(value, "\n")
for _, line := range lines {
if strings.HasPrefix(line, namespaceProp) {
tokens := strings.SplitN(line, ":", 2)
if len(tokens) < 2 {
return nil, errs.New("malformed environments config map")
}
namespace = strings.TrimSpace(tokens[1])
}
}
if len(namespace) == 0 {
return nil, errs.Errorf("no namespace for environment %s in config map", key)
}
envMap[key] = namespace
}
return envMap, nil
}

func (kc *kubeClient) getEnvironmentNamespace(envName string) (string, error) {
envNS, pres := kc.envMap[envName]
if !pres {
Expand Down
93 changes: 11 additions & 82 deletions kubernetes/deployments_kubeclient_blackbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,25 +197,19 @@ func TestGetMetrics(t *testing.T) {
name string
clusterURL string
expectedURL string
cassetteName string
shouldSucceed bool
}{
{"Basic", "https://api.myCluster.url:443/cluster", "https://metrics.myCluster.url", "newkubeclient-withport", true},
{"Bad URL", "https://myCluster.url:443/cluster", "", "newkubeclient-badurl", false},
{"Basic", "https://api.myCluster.url:443/cluster", "https://metrics.myCluster.url", true},
{"Bad URL", "https://myCluster.url:443/cluster", "", false},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
r, err := recorder.New(pathToTestJSON + testCase.cassetteName)
require.NoError(t, err, "Failed to open cassette")
defer r.Stop()

fixture := &testFixture{}
config := &kubernetes.KubeClientConfig{
BaseURLProvider: getDefaultURLProvider(testCase.clusterURL, token),
UserNamespace: "myNamespace",
MetricsGetter: fixture,
Transport: r.Transport,
}
kc, err := kubernetes.NewKubeClient(config)
if testCase.shouldSucceed {
Expand All @@ -242,12 +236,8 @@ var _ kubernetes.MetricsGetter = &testFixture{}
var _ kubernetes.MetricsGetter = (*testFixture)(nil)

func TestClose(t *testing.T) {
r, err := recorder.New(pathToTestJSON + "newkubeclient")
require.NoError(t, err, "Failed to open cassette")
defer r.Stop()

fixture := &testFixture{}
kc := getDefaultKubeClient(fixture, r.Transport, t)
kc := getDefaultKubeClient(fixture, nil, t)

mm, err := kc.GetMetricsClient("myNamespace")
require.NoError(t, err)
Expand All @@ -259,75 +249,6 @@ func TestClose(t *testing.T) {
require.True(t, fixture.metrics.closed, "Metrics client not closed")
}

func TestConfigMapEnvironments(t *testing.T) {
testCases := []struct {
name string
cassetteName string
shouldFail bool
// Checks if error is expected kind
errorChecker func(error) (bool, error)
}{
{
name: "Basic",
cassetteName: "newkubeclient",
},
{
name: "Empty Data",
cassetteName: "newkubeclient-empty",
},
{
name: "Missing Colon",
cassetteName: "newkubeclient-nocolon",
shouldFail: true,
},
{
name: "Missing Namespace",
cassetteName: "newkubeclient-nonamespace",
shouldFail: true,
},
{
name: "No Provider",
cassetteName: "newkubeclient-noprovider",
shouldFail: true,
},
{
name: "Kubernetes Error",
cassetteName: "newkubeclient-statuserror",
shouldFail: true,
errorChecker: errors.IsNotFoundError,
},
}
fixture := &testFixture{}
userNamespace := "myNamespace"
config := &kubernetes.KubeClientConfig{
BaseURLProvider: getDefaultURLProvider("http://api.myCluster", "myToken"),
UserNamespace: userNamespace,
MetricsGetter: fixture,
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
r, err := recorder.New(pathToTestJSON + testCase.cassetteName)
require.NoError(t, err, "Failed to open cassette")
defer r.Stop()

config.Transport = r.Transport

kc, err := kubernetes.NewKubeClient(config)
if testCase.shouldFail {
require.Error(t, err, "Expected an error")
if testCase.errorChecker != nil {
matches, _ := testCase.errorChecker(err)
require.True(t, matches, "Error or cause must be the expected type")
}
} else {
require.NoError(t, err)
require.NotNil(t, kc, "KubeClient must not be nil")
}
})
}
}

type envTestData struct {
envName string
cpuUsed float64
Expand Down Expand Up @@ -1552,6 +1473,14 @@ func (up *testURLProvider) GetMetricsURL(envNS string) (*string, error) {
return &mu, nil
}

func (up *testURLProvider) GetEnvironmentMapping() map[string]string {
return map[string]string{
"test": "myNamespace",
"run": "my-run",
"stage": "my-stage",
}
}

func modifyURL(apiURLStr string, prefix string, path string) (*url.URL, error) {
// Parse as URL to give us easy access to the hostname
apiURL, err := url.Parse(apiURLStr)
Expand Down
8 changes: 8 additions & 0 deletions kubernetes/deployments_kubeclient_whitebox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,11 @@ func (up *testURLProvider) GetLoggingURL(envNS string, deployName string) (*stri
func (up *testURLProvider) GetMetricsURL(envNS string) (*string, error) {
return &up.apiURL, nil
}

func (up *testURLProvider) GetEnvironmentMapping() map[string]string {
return map[string]string{
"test": "myNamespace",
"run": "my-run",
"stage": "my-stage",
}
}
45 changes: 0 additions & 45 deletions test/kubernetes/deletedeployment-badroutes.yaml
Original file line number Diff line number Diff line change
@@ -1,51 +1,6 @@
---
version: 1
interactions:
# Environments ConfigMap
- request:
body: ""
form: {}
headers:
Content-Type:
- application/json
url: http://api.myCluster/api/v1/namespaces/myNamespace/configmaps/fabric8-environments
method: GET
response:
body: |
{
"apiVersion": "v1",
"data": {
"run": "name: Run\nnamespace: my-run\norder: 2",
"stage": "name: Stage\nnamespace: my-stage\norder: 1",
"test": "name: Test\nnamespace: myNamespace\norder: 0"
},
"kind": "ConfigMap",
"metadata": {
"annotations": {
"description": "Defines the environments used by your Continuous Delivery pipelines.",
"fabric8.console/iconUrl": "https://cdn.rawgit.com/fabric8io/fabric8-console/master/app-kubernetes/src/main/fabric8/icon.svg",
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"data\":{\"run\":\"name: Run\\nnamespace: my-run\\norder: 2\",\"stage\":\"name: Stage\\nnamespace: my-stage\\norder: 1\",\"test\":\"name: Test\\nnamespace: myNamespace\\norder: 0\"},\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{\"description\":\"Defines the environments used by your Continuous Delivery pipelines.\",\"fabric8.console/iconUrl\":\"https://cdn.rawgit.com/fabric8io/fabric8-console/master/app-kubernetes/src/main/fabric8/icon.svg\"},\"labels\":{\"app\":\"fabric8-tenant-team\",\"group\":\"io.fabric8.tenant.packages\",\"kind\":\"environments\",\"provider\":\"fabric8\",\"version\":\"2.0.11\"},\"name\":\"fabric8-environments\",\"namespace\":\"myNamespace\"}}\n"
},
"creationTimestamp": "2018-02-26T18:00:45Z",
"labels": {
"app": "fabric8-tenant-team",
"group": "io.fabric8.tenant.packages",
"kind": "environments",
"provider": "fabric8",
"version": "2.0.11"
},
"name": "fabric8-environments",
"namespace": "myNamespace",
"resourceVersion": "996068051",
"selfLink": "/api/v1/namespaces/myNamespace/configmaps/fabric8-environments",
"uid": "f808e2e2-1b1e-11e8-ae91-0233cba325d9"
}
}
headers:
Content-Type:
- application/json;charset=UTF-8
status: 200 OK
code: 200
# Builds
- request:
body: ""
Expand Down
Loading