diff --git a/.circleci/config.yml b/.circleci/config.yml index 65df943..98d8fb0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,7 +19,11 @@ jobs: - run: name: Install Glide command: curl https://glide.sh/get | sh + - run: + name: Install Mockery + command: go get github.com/vektra/mockery/.../ - run: make setup + - run: make test - run: make build_linux docker_push_master: diff --git a/Makefile b/Makefile index 27e1669..ed594d3 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,10 @@ APP_NAME = ups-config-operator + +PKG = github.com/aerogear/$(APP_NAME) +TOP_SRC_DIRS = pkg +PACKAGES ?= $(shell sh -c "find $(TOP_SRC_DIRS) -name \\*_test.go \ + -exec dirname {} \\; | sort | uniq") + DOCKER_LATEST_TAG = docker.io/aerogear/$(APP_NAME):latest DOCKER_MASTER_TAG = docker.io/aerogear/$(APP_NAME):master RELEASE_TAG ?= $(CIRCLE_TAG) @@ -10,11 +16,20 @@ generate: .PHONY: setup setup: - glide install --strip-vendor + glide install + mockery -all -inpkg -dir pkg + +.PHONY: test +test: + @echo Running tests: + mockery -all -inpkg -dir pkg + GOCACHE=off go test -cover \ + $(addprefix $(PKG)/,$(PACKAGES)) .PHONY: build_linux build_linux: - env GOOS=linux GOARCH=amd64 go build cmd/server/main.go cmd/server/types.go cmd/server/upsClient.go + mockery -all -inpkg -dir pkg + env GOOS=linux GOARCH=amd64 go build cmd/server/main.go .PHONY: docker_build docker_build: build_linux diff --git a/README.md b/README.md index daf7289..df406f2 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,12 @@ Currently this needs to use a service account with admin permissions. Use: $ kubectl create clusterrolebinding -admin-binding --clusterrole=admin --serviceaccount=:default ``` +# Development: + +* Install Mockery on your machine: +* Run `make setup` +* Run tests: `make test` + ## Usage Make sure that you are logged in with `oc` and use the right namespace. diff --git a/cmd/server/main.go b/cmd/server/main.go index 4e3346e..79369e9 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,68 +2,22 @@ package main import ( "os" - "strconv" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "encoding/json" - "log" - "github.com/satori/go.uuid" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" - "fmt" "math/rand" - "strings" "time" mc "github.com/aerogear/mobile-crd-client/pkg/client/mobile/clientset/versioned" sc "github.com/aerogear/mobile-crd-client/pkg/client/servicecatalog/clientset/versioned" - "github.com/pkg/errors" - "k8s.io/client-go/pkg/api/v1" -) - -var mobileclient *mc.Clientset -var k8client *kubernetes.Clientset -var scclient *sc.Clientset -var pushClient *upsClient - -const NamespaceKey = "NAMESPACE" -const ActionAdded = "ADDED" -const ActionDeleted = "DELETED" -const SecretTypeKey = "secretType" -const ServiceInstanceIdKey = "serviceInstanceID" -const ServiceBindingIdKey = "serviceBindingId" -const ServiceInstanceNameKey = "serviceInstanceName" - -const BindingSecretType = "mobile-client-binding-secret" -const BindingAppType = "appType" -const BindingClientId = "clientId" -const BindingGoogleKey = "googleKey" -const BindingProjectNumber = "projectNumber" -const UpsSecretName = "unified-push-server" -const UpsURI = "uri" -const IOSCert = "cert" -const IOSPassPhrase = "passphrase" -const IOSIsProduction = "isProduction" - -// time in seconds -const UPSPollingInterval = 10 - -var letters = []rune("abcdefghijklmnopqrstuvwxyz0123456789") - -// This is required because importing core/v1/Secret leads to a double import and redefinition -// of log_dir -type BindingSecret struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` - Data map[string][]byte `json:"data,omitempty" protobuf:"bytes,2,rep,name=data"` - StringData map[string]string `json:"stringData,omitempty" protobuf:"bytes,4,rep,name=stringData"` -} + "github.com/aerogear/ups-config-operator/pkg/configOperator" + "github.com/aerogear/ups-config-operator/pkg/constants" +) func main() { rand.Seed(time.Now().Unix()) @@ -73,561 +27,41 @@ func main() { panic(err.Error()) } - clientsOrDie(config) - - log.Print("Entering watch loop") - - go startPollingUPS() - startWatchLoop() -} - -func startWatchLoop() { - events, err := k8client.CoreV1().Secrets(os.Getenv(NamespaceKey)).Watch(metav1.ListOptions{}) - if err != nil { - panic(err.Error()) - } - - for update := range events.ResultChan() { - switch action := update.Type; action { - case ActionAdded: - handleAddSecret(update.Object) - case ActionDeleted: - handleDeleteSecret(update.Object) - default: - log.Print("Unhandled action:", action) - } - } -} - -func handleAddSecret(obj runtime.Object) { - raw, _ := json.Marshal(obj) - var secret = BindingSecret{} - json.Unmarshal(raw, &secret) - if val, ok := secret.Labels[SecretTypeKey]; ok && val == BindingSecretType { - appType := string(secret.Data[BindingAppType]) - log.Printf("A mobile binding secret of type `%s` was added", appType) - - if appType == "Android" { - handleAndroidVariant(&secret) - } else if appType == "IOS" { - handleIOSVariant(&secret) - } - // Always delete the secret after handling it regardless of any new resources - // was created - deleteSecret(secret.Name) - } -} - -func handleDeleteSecret(obj runtime.Object) { - raw, _ := json.Marshal(obj) - var secret = BindingSecret{} - json.Unmarshal(raw, &secret) - - for _, ref := range secret.ObjectMeta.OwnerReferences { - if ref.Kind == "ServiceBinding" { - handleDeleteVariant(&secret) - break - } - } -} - -// startPollingUPS() is a loop that calls comparseUPSVariantsWithClientConfigs() in intervals -func startPollingUPS() { - interval := UPSPollingInterval * time.Second - for { - <-time.After(interval) - compareUPSVariantsWithClientConfigs() - } -} - -// compareUPSVariantsWithClientConfigs() compares the UPS client configs stored in k8's secrets -// against the variants in UPS in order to detect if a variant has been deleted in UPS -// If a client config is found that references a variant not found in UPS then we clean up the client config by deleting the associated servicebinding. -func compareUPSVariantsWithClientConfigs() { - - err := initPushClient() - - if err != nil { - log.Printf("error initialising UPS client: %s", err.Error()) - return - } - - // get the UPS related secrets - secrets, err := getUPSSecrets() - - if err != nil { - log.Printf("Error searching for ups secrets: %v", err.Error()) - return - } - - // process the secrets into a list of VariantServiceBindingMappings - // each element has VariantId and ServiceBindingId - clientConfigs := getUPSVariantServiceBindingMappings(secrets) - - // Get all variants from UPS - UPSVariants, err := pushClient.getVariants() - - if err != nil { - log.Printf("An error occurred trying to get variants from UPS service: %v", err.Error()) - return - } - - for _, clientConfig := range clientConfigs { - found := false - - for _, variant := range UPSVariants { - if variant.VariantID == clientConfig.VariantId { - found = true - break - } - } - - if !found { - fmt.Printf("variant Id %v found in client configs but not found in UPS. Should delete", clientConfig.VariantId) - err := handleDeleteServiceBinding(clientConfig.ServiceBindingId) - if err != nil { - log.Printf("Error deleting service binding instance with id %s\n%s", clientConfig.ServiceBindingId, err.Error()) - } - } - } -} - -// getUPSSecrets() returns a list of the secrets that contain the UPS client configs -func getUPSSecrets() ([]v1.Secret, error) { - selector := fmt.Sprintf("serviceName=ups,pushApplicationId=%s", pushClient.config.ApplicationId) - filter := metav1.ListOptions{LabelSelector: selector} - secretsList, err := k8client.CoreV1().Secrets(os.Getenv(NamespaceKey)).List(filter) - return secretsList.Items, err -} - -// getUPSVariantServiceBindingMappings() takes the list of secrets and returns a list of VariantServiceBindingMappings -func getUPSVariantServiceBindingMappings(secrets []v1.Secret) []VariantServiceBindingMapping { - - var results []VariantServiceBindingMapping - - buildAndAppendResult := func(results []VariantServiceBindingMapping, variantId string, serviceBindingId string, secret v1.Secret) []VariantServiceBindingMapping { - if variantServiceBindingMapping, err := GetClientConfigRepresentation(variantId, serviceBindingId); err != nil { - log.Printf("invalid android UPS client config found in secret %s reason: %s", secret.Name, err.Error()) - return results - } else { - return append(results, variantServiceBindingMapping) - } - } - - for _, secret := range secrets { - - // Retrieve the current config as an object - clientConfig := UPSClientConfig{} - json.Unmarshal(secret.Data["config"], &clientConfig) - - if clientConfig.Android != nil { - androidConfig := *clientConfig.Android - variantId := androidConfig["variantId"] - serviceBindingId := secret.ObjectMeta.Annotations["binding/android"] - results = buildAndAppendResult(results, variantId, serviceBindingId, secret) - } - - if clientConfig.IOS != nil { - iOSConfig := *clientConfig.IOS - variantId := iOSConfig["variantId"] - serviceBindingId := secret.ObjectMeta.Annotations["binding/ios"] - results = buildAndAppendResult(results, variantId, serviceBindingId, secret) - } - } - return results -} - -func handleDeleteServiceBinding(servicebindingId string) error { - serviceBindingName, err := getServiceBindingNameByID(servicebindingId) - if err != nil { - return err - } - err = deleteServiceBinding(serviceBindingName) - return err -} - -// Find a service binding by its ExternalID -func getServiceBindingNameByID(bindingId string) (string, error) { - // Get a list of all service bindings in the namespace and find the one with a matching ExternalID - // This is not very efficient and could be improved with a jsonpath query but it looks like client-go - // does not support jsonpath or at least I could not find any examples. - bindings, err := scclient.ServicecatalogV1beta1().ServiceBindings(os.Getenv(NamespaceKey)).List(metav1.ListOptions{}) - if err != nil { - return "", err - } - - for _, binding := range bindings.Items { - log.Printf("Checking service binding %s", binding.Name) - if binding.Spec.ExternalID == bindingId { - return binding.Name, nil - } - } - - return "", errors.New(fmt.Sprintf("Can't find a binding with ExternalID %s", bindingId)) -} - -func deleteServiceBinding(bindingName string) error { - return scclient.ServicecatalogV1beta1().ServiceBindings(os.Getenv(NamespaceKey)).Delete(bindingName, nil) -} - -// Create a random identifier of the given length. Useful for randomized resource names -func getRandomIdentifier(length int) string { - result := make([]rune, length) - for i := 0; i < length; i++ { - result[i] = letters[rand.Intn(len(letters))] - } - - return string(result) -} - -// Deletes a secret -func deleteSecret(name string) { - err := k8client.CoreV1().Secrets(os.Getenv(NamespaceKey)).Delete(name, nil) - - if err != nil { - log.Print("Error deleting secret", err) - } else { - log.Printf("Secret `%s` has been deleted", name) - } -} - -func handleAndroidVariant(secret *BindingSecret) { - - err := initPushClient() - - if err != nil { - log.Printf("error initialising UPS client: %s", err.Error()) - return - } - - clientId := string(secret.Data[BindingClientId]) - googleKey := string(secret.Data[BindingGoogleKey]) - projectNumber := string(secret.Data[BindingProjectNumber]) - serviceBindingId := string(secret.Data[ServiceBindingIdKey]) - serviceInstanceName := string(secret.Data[ServiceInstanceNameKey]) - - payload := &androidVariant{ - ProjectNumber: projectNumber, - GoogleKey: googleKey, - variant: variant{ - Name: clientId, - VariantID: uuid.NewV4().String(), - Secret: uuid.NewV4().String(), - }, - } - - log.Print("Creating a new android variant", payload) - success, variant := pushClient.createAndroidVariant(payload) - if success { - config, _ := variant.getJson() - updateConfiguration("android", clientId, variant.VariantID, config, serviceBindingId, serviceInstanceName) - } else { - log.Println("No variant has been created in UPS, skipping config secret") - } -} - -func handleIOSVariant(secret *BindingSecret) { + k8client := kubernetes.NewForConfigOrDie(config) + scclient := sc.NewForConfigOrDie(config) + mobileclient := mc.NewForConfigOrDie(config) - err := initPushClient() + pushClient, err := createPushClient(k8client) if err != nil { log.Printf("error initialising UPS client: %s", err.Error()) return } - clientId := string(secret.Data[BindingClientId]) - cert := string(secret.Data[IOSCert]) - passPhrase := string(secret.Data[IOSPassPhrase]) - serviceBindingId := string(secret.Data[ServiceBindingIdKey]) - serviceInstanceName := string(secret.Data[ServiceInstanceNameKey]) - isProductionString := string(secret.Data[IOSIsProduction]) - isProduction, err := strconv.ParseBool(isProductionString) - - if err != nil { - log.Printf("iOS variant with clientId %v is invalid, isProduction value %v should be true or false. Setting to false", clientId, isProductionString) - isProduction = false - } - - certByteArray := []byte(cert) - payload := &iOSVariant{ - Certificate: certByteArray, - Passphrase: passPhrase, - Production: isProduction, //false for now while testing functionality - variant: variant{ - Name: clientId, - VariantID: uuid.NewV4().String(), - Secret: uuid.NewV4().String(), - }, - } - - success, variant := pushClient.createIOSVariant(payload) - if success { - config, _ := variant.getJson() - updateConfiguration("ios", clientId, variant.VariantID, config, serviceBindingId, serviceInstanceName) - } else { - log.Print("No variant has been created in UPS, skipping config secret") - } -} - -// Deletes a configuration from the config secret and from the UPS server -func handleDeleteVariant(secret *BindingSecret) { - appType := strings.ToLower(string(secret.Data["appType"])) - success, variantId := removeConfigFromClientSecret(secret, appType) - - if success { - success := pushClient.deleteVariant(appType, variantId) - if !success { - log.Printf("UPS reported an error when deleting variant %s", variantId) - } - } -} - -// Find a mobile client bound ups config secret -func findMobileClientConfig(clientId string) *v1.Secret { - filter := metav1.ListOptions{LabelSelector: fmt.Sprintf("clientId=%s,serviceName=ups", clientId)} - secrets, err := k8client.CoreV1().Secrets(os.Getenv(NamespaceKey)).List(filter) - - if err != nil { - panic(err.Error()) - } - - // No secret exists yet, that's ok, we have to create one - if len(secrets.Items) == 0 { - return nil - } - - // Multiple secrets for the same clientId found, that's an error - if len(secrets.Items) > 1 { - panic(fmt.Sprintf("Multiple secrets found for clientId %s", clientId)) - } + annotationHelper := configOperator.NewAnnotationHelper(mobileclient) - return &secrets.Items[0] -} + kubeHelper := configOperator.NewKubeHelper(k8client, scclient) -// Creates the JSON string for the mobile-client variant annotation -func generateVariantAnnotationValue(url string, appType string) ([]byte, error) { - annotation := VariantAnnotation{ - Type: "href", - Label: fmt.Sprintf("UPS %s Variant", appType), - Value: url, - } + operator := configOperator.NewConfigOperator(pushClient, annotationHelper, kubeHelper) - return json.Marshal(annotation) + operator.StartService() } -// Adds an annotation to the mobile client that contains information about this variant -// (currently URL and Name) -func addAnnotationToMobileClient(clientId string, appType string, variantUrl string, serviceInstanceName string) { - client, err := mobileclient.MobileV1alpha1().MobileClients(os.Getenv(NamespaceKey)).Get(clientId, metav1.GetOptions{}) - if err != nil { - log.Printf("No mobile client with name %s found", clientId) - return - } - - annotationName := fmt.Sprintf("org.aerogear.binding.%s/variant-%s", serviceInstanceName, appType) - annotationValue, err := generateVariantAnnotationValue(variantUrl, appType) - if err != nil { - log.Printf(err.Error()) - return - } - - if client.Annotations == nil { - client.Annotations = make(map[string]string) - } - - client.Annotations[annotationName] = string(annotationValue) - _, err = mobileclient.MobileV1alpha1().MobileClients(os.Getenv(NamespaceKey)).Update(client) - if err != nil { - log.Printf(err.Error()) - } -} +func createPushClient(k8client *kubernetes.Clientset) (*configOperator.UpsClientImpl, error) { + upsSecret, err := k8client.CoreV1().Secrets(os.Getenv(constants.EnvVarKeyNamespace)).Get(constants.UpsSecretName, metav1.GetOptions{}) -func removeAnnotationFromMobileClient(clientId string, appType string, serviceInstanceName string) { - client, err := mobileclient.MobileV1alpha1().MobileClients(os.Getenv(NamespaceKey)).Get(clientId, metav1.GetOptions{}) if err != nil { - log.Printf("No mobile client with name %s found", clientId) - return + return &configOperator.UpsClientImpl{}, err } - if client.Annotations != nil { - annotationName := fmt.Sprintf("org.aerogear.binding.%s/variant-%s", serviceInstanceName, appType) - log.Printf("Removing annotation %s from mobile client %s", annotationName, clientId) + upsBaseURL := string(upsSecret.Data[constants.UpsSecretDataUrlKey]) + serviceInstanceId := upsSecret.Labels[constants.UpsSecretLabelServiceInstanceIdKey] - delete(client.Annotations, annotationName) - _, err = mobileclient.MobileV1alpha1().MobileClients(os.Getenv(NamespaceKey)).Update(client) - if err != nil { - log.Printf(err.Error()) - } + config := &configOperator.PushApplication{ + ApplicationId: string(upsSecret.Data["applicationId"]), } -} - -// Creates a mobile client bound ups config secret -func createClientConfigSecret(clientId string, serviceInstanceName string) *v1.Secret { - configSecretName := fmt.Sprintf("ups-secret-%s-%s", clientId, getRandomIdentifier(5)) - - payload := v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: configSecretName, - Labels: map[string]string{ - "mobile": "enabled", - "serviceName": "ups", - // Used by the mobile-cli to discover config objects - "serviceInstanceId": pushClient.serviceInstanceId, - "clientId": clientId, - "pushApplicationId": pushClient.config.ApplicationId, - }, - }, - Data: map[string][]byte{ - // Used to generate the name of the UI annotations - ServiceInstanceNameKey: []byte(serviceInstanceName), - "config": []byte("{}"), - }, - } - - secret, err := k8client.CoreV1().Secrets(os.Getenv(NamespaceKey)).Create(&payload) - if err != nil { - log.Fatal("Error creating ups config secret", err) - } else { - log.Printf("Config secret `%s` for variant created", configSecretName) - } - - return secret -} - -// Removes a platform configuration (e.g. iOS or Android) from the `Data.config` map of a UPS configuration -// secret. If there is only one platform it will delete the whole secret. -func removeConfigFromClientSecret(secret *BindingSecret, appType string) (bool, string) { - clientId := string(secret.Data["clientId"]) - configSecret := findMobileClientConfig(clientId) - - if configSecret == nil { - log.Printf("Cannot delete configuration for client `%s` because the secret does not exist", clientId) - return false, "" - } - - serviceInstanceName := string(configSecret.Data[ServiceInstanceNameKey]) - log.Printf("Deleting %s configuration from %s", appType, clientId) - - // Remove the annotation also from the mobile client - removeAnnotationFromMobileClient(clientId, appType, serviceInstanceName) - - // Get the current config - // Retrieve the current config as an object - var currentConfig map[string]json.RawMessage - json.Unmarshal(configSecret.Data["config"], ¤tConfig) - - // Get the variant ID before removing the config - // We need that to delete the variant in UPS - variantId := getVariantIdFromConfig(string(currentConfig[appType])) - - // If there is only one platform in the configuration we can remove the whole - // secret - if len(currentConfig) == 1 { - deleteSecret(configSecret.Name) - return true, variantId - } else { - log.Println("More than one variant available, updating configuration object") - - // Delete the config of the given app type and it's annotations - delete(currentConfig, appType) - delete(configSecret.Annotations, fmt.Sprintf("binding/%s", appType)) - - // Create a string of the new config object - currentConfigString, err := json.Marshal(currentConfig) - if err != nil { - panic(err.Error()) - } - - configSecret.Data["config"] = currentConfigString - _, err = k8client.CoreV1().Secrets(os.Getenv(NamespaceKey)).Update(configSecret) - if err != nil { - log.Println(err.Error()) - } - - return true, variantId - } -} - -func getVariantIdFromConfig(config string) string { - configMap := make(map[string]string) - json.Unmarshal([]byte(config), &configMap) - return configMap["variantId"] -} - -// Updates the `Data.config` map of a UPS configuration secret -// The secret can contain multiple variants (e.g. iOS and Android) but is bound to one mobile client -func updateConfiguration(appType string, clientId string, variantId string, newConfig []byte, bindingId string, serviceInstanceName string) { - configSecret := findMobileClientConfig(clientId) - if configSecret == nil { - // No config secret exists for this client yet. Create one. - configSecret = createClientConfigSecret(clientId, serviceInstanceName) - } - - // Retrieve the current config as an object - var currentConfig map[string]json.RawMessage - json.Unmarshal(configSecret.Data["config"], ¤tConfig) - - // Overwrite the old platform config - currentConfig[appType] = []byte(newConfig) - - // Create a string of the complete config object - currentConfigString, err := json.Marshal(currentConfig) - if err != nil { - panic(err.Error()) - } - - // Set the new config - configSecret.Data["uri"] = []byte(pushClient.baseUrl) - configSecret.Data["config"] = currentConfigString - configSecret.Data["name"] = []byte("ups") - configSecret.Data["type"] = []byte("push") - - // Add the binding annotation to the UPS secret: this is done to link the actual ServiceBinding - // Instance back to this secret. In case the variant is deleted in UPS we can use this ID to delete - // the service binding - bindingAnnotation := fmt.Sprintf("binding/%s", appType) - if configSecret.Annotations == nil { - configSecret.Annotations = make(map[string]string) - } - configSecret.Annotations[bindingAnnotation] = bindingId - - // Annotate the mobile client with the variant URL. This is done to display a link to - // the variant in the Mobile Client UI in Openshift - variantUrl := pushClient.baseUrl + "/#/app/" + pushClient.config.ApplicationId + "/variants/" + variantId - addAnnotationToMobileClient(clientId, appType, variantUrl, serviceInstanceName) - - k8client.CoreV1().Secrets(os.Getenv(NamespaceKey)).Update(configSecret) - log.Printf("%s configuration of %s has been updated", appType, clientId) -} - -func clientsOrDie(config *rest.Config) { - k8client = kubernetes.NewForConfigOrDie(config) - scclient = sc.NewForConfigOrDie(config) - mobileclient = mc.NewForConfigOrDie(config) -} - -func initPushClient() error { - if pushClient != nil { - return nil - } - - upsSecret, err := k8client.CoreV1().Secrets(os.Getenv(NamespaceKey)).Get(UpsSecretName, metav1.GetOptions{}) - - if err != nil { - return err - } - - upsBaseURL := string(upsSecret.Data[UpsURI]) - serviceInstanceId := upsSecret.Labels[ServiceInstanceIdKey] - - pushClient = &upsClient{ - config: &pushApplication{ - ApplicationId: string(upsSecret.Data["applicationId"]), - }, - serviceInstanceId: serviceInstanceId, - baseUrl: upsBaseURL, - } + pushClient := configOperator.NewUpsClientImpl(config, serviceInstanceId, upsBaseURL) - return nil + return pushClient, nil } diff --git a/glide.lock b/glide.lock index a0ae56e..58e04f0 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 0208f33376d626e7dae35d7fab66c46a1308b8174f757f5a16feb3f4f14b615e -updated: 2018-05-14T15:31:14.766614+01:00 +hash: fb4a11e35da439b7f4d9eeb6b900babe25c9684da4363ec1266a36dd88ee22dd +updated: 2018-05-25T18:18:50.48579068+03:00 imports: - name: github.com/aerogear/mobile-crd-client version: 8ea4fe5f0a4cefe1e2c50c9fe6fa5db1d7e38340 @@ -66,6 +66,10 @@ imports: version: 65fec0d89a572b4367094e2058d3ebe667de3b60 - name: github.com/pkg/errors version: 645ef00459ed84a119197bfb8d8205042c6df63d +- name: github.com/pmezard/go-difflib + version: d8ed2627bdf02c080bf22230dbb337003b7aba2d + subpackages: + - difflib - name: github.com/PuerkitoBio/purell version: 8a290539e2e8629dbc4e6bad948158f790ec31f4 - name: github.com/PuerkitoBio/urlesc @@ -78,6 +82,13 @@ imports: version: e57e3eeb33f795204c1ca35f56c44f83227c6e66 - name: github.com/spf13/viper version: 4dddf7c62e16bce5807744018f5b753bfe21bbd2 +- name: github.com/stretchr/objx + version: a5cfa15c000af5f09784e5355969ba7eb66ef0de +- name: github.com/stretchr/testify + version: 12b6f73e6084dad08a7c6e575284b177ecafbc71 + subpackages: + - assert + - mock - name: github.com/ugorji/go version: ded73eae5db7e7a0ef6f55aace87a2873c5d2b74 subpackages: diff --git a/glide.yaml b/glide.yaml index 207ad79..fd47273 100644 --- a/glide.yaml +++ b/glide.yaml @@ -44,3 +44,5 @@ import: subpackages: - pkg/client/mobile/clientset/versioned - pkg/client/servicecatalog/clientset/versioned +- package: github.com/stretchr/testify + version: v1.2.1 diff --git a/pkg/configOperator/annotationHelper.go b/pkg/configOperator/annotationHelper.go new file mode 100644 index 0000000..ffd2be9 --- /dev/null +++ b/pkg/configOperator/annotationHelper.go @@ -0,0 +1,184 @@ +package configOperator + +import ( + "fmt" + "os" + "log" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + mc "github.com/aerogear/mobile-crd-client/pkg/client/mobile/clientset/versioned" + "github.com/aerogear/ups-config-operator/pkg/constants" + "encoding/json" + "strings" +) + +type AnnotationHelper interface { + addAnnotationToMobileClient(clientId string, upsUrl string, pushApplicationId string, pushApplicationName string, appType string, variantUrl string, serviceInstanceName string) + removeAnnotationFromMobileClient(clientId string, appType string, serviceInstanceName string) +} + +type AnnotationHelperImpl struct { + mobileclient *mc.Clientset +} + +func NewAnnotationHelper(mobileclient *mc.Clientset) *AnnotationHelperImpl { + helper := new(AnnotationHelperImpl) + + helper.mobileclient = mobileclient + + return helper +} + +// Adds an annotation to the mobile client that contains information about this variant +// (currently URL and Name) +func (helper AnnotationHelperImpl) addAnnotationToMobileClient(clientId string, upsUrl string, pushApplicationId string, pushApplicationName string, appType string, variantId string, serviceInstanceName string) { + client, err := helper.mobileclient.MobileV1alpha1().MobileClients(os.Getenv(constants.EnvVarKeyNamespace)).Get(clientId, metav1.GetOptions{}) + if err != nil { + log.Printf("No mobile client with name %s found", clientId) + return + } + + pushApplicationUrl := upsUrl + "/#/app/" + pushApplicationId + "/variants" + variantUrl := pushApplicationUrl + "/" + variantId + + pushAppAnnotationName := fmt.Sprintf(constants.PushAppAnnotationNameFormat, serviceInstanceName) + pushAppAnnotationValue := fmt.Sprintf(`{"label":"Push Application","type":"href","text":"%s", "value":"%s"}`, pushApplicationName, pushApplicationUrl) + + upsUrlAnnotationName := fmt.Sprintf(constants.UpsUrlAnnotationNameFormat, serviceInstanceName) + upsUrlAnnotationValue := fmt.Sprintf(`{"label":"URL","type":"href","value":"%s"}`, upsUrl) + + extVariantAnnotationName := fmt.Sprintf(constants.ExtVariantsAnnotationNameFormat, serviceInstanceName) + extVariantAnnotationConfigForSingleVariant := variantAnnotationConfig{ + Type: appType, + TypeLabel: getVariantTypeLabel(appType), + Url: variantUrl, + Id: variantId, + } + var extVariantAnnotationConfigValue []variantAnnotationConfig + + if client.Annotations == nil { + client.Annotations = make(map[string]string) + } + + client.Annotations[pushAppAnnotationName] = pushAppAnnotationValue + client.Annotations[upsUrlAnnotationName] = upsUrlAnnotationValue + + extVariantAnnotationConfigValue = []variantAnnotationConfig{ + extVariantAnnotationConfigForSingleVariant, + } + + if client.Annotations[extVariantAnnotationName] != "" { + var existingVariantConfigs []variantAnnotationConfig + err = json.Unmarshal([]byte(client.Annotations[extVariantAnnotationName]), &existingVariantConfigs) + + if err != nil { + log.Printf("Error unmarshalling variant annotation config for name %s. Error: %s", client.Annotations[extVariantAnnotationName], err.Error()) + return + } + + for _, variantConfig := range existingVariantConfigs { + if variantConfig.Type == appType { + continue + } else { + extVariantAnnotationConfigValue = append(extVariantAnnotationConfigValue, variantConfig) + } + } + } + + extVariantAnnotationConfigValueStr, err := json.Marshal(extVariantAnnotationConfigValue) + + if err != nil { + log.Printf("Error marshalling newly built variant annotation config for name %s. Value: %s, Error: %s", extVariantAnnotationConfigValueStr, extVariantAnnotationConfigValue, err.Error()) + return + } + + client.Annotations[extVariantAnnotationName] = string(extVariantAnnotationConfigValueStr) + + _, err = helper.mobileclient.MobileV1alpha1().MobileClients(os.Getenv(constants.EnvVarKeyNamespace)).Update(client) + if err != nil { + log.Printf(err.Error()) + } +} + +func (helper AnnotationHelperImpl) removeAnnotationFromMobileClient(clientId string, appType string, serviceInstanceName string) { + client, err := helper.mobileclient.MobileV1alpha1().MobileClients(os.Getenv(constants.EnvVarKeyNamespace)).Get(clientId, metav1.GetOptions{}) + if err != nil { + log.Printf("No mobile client with name %s found", clientId) + return + } + + if client.Annotations == nil { + log.Printf("Mobile client doesn't have any annotations. Returning w/o any operation") + return + } + + extVariantAnnotationName := fmt.Sprintf(constants.ExtVariantsAnnotationNameFormat, serviceInstanceName) + + var existingVariantConfigs []variantAnnotationConfig + err = json.Unmarshal([]byte(client.Annotations[extVariantAnnotationName]), &existingVariantConfigs) + + if err != nil { + log.Printf("Error unmarshalling variant annotation config for name %s. Error: %s", client.Annotations[extVariantAnnotationName], err.Error()) + return + } + + var extVariantAnnotationConfigValue = []variantAnnotationConfig{} + + for _, variantConfig := range existingVariantConfigs { + if variantConfig.Type == appType { + continue + } else { + extVariantAnnotationConfigValue = append(extVariantAnnotationConfigValue, variantConfig) + } + } + + if len(extVariantAnnotationConfigValue) > 0 { + newConfigStr, err := json.Marshal(extVariantAnnotationConfigValue) + + if err != nil { + log.Printf("Error marshalling newly built variant annotation config for name %s. Value: %s, Error: %s", client.Annotations[extVariantAnnotationName], extVariantAnnotationConfigValue, err.Error()) + return + } + + client.Annotations[extVariantAnnotationName] = string(newConfigStr) + + _, err = helper.mobileclient.MobileV1alpha1().MobileClients(os.Getenv(constants.EnvVarKeyNamespace)).Update(client) + if err != nil { + log.Printf("Unable to update mobile client %s. Error: %s", clientId, err.Error()) + } + + } else { + log.Println("Removing all push related annotations from the mobile client as there are no variants anymore") + + pushAppAnnotationName := fmt.Sprintf(constants.PushAppAnnotationNameFormat, serviceInstanceName) + upsUrlAnnotationName := fmt.Sprintf(constants.UpsUrlAnnotationNameFormat, serviceInstanceName) + + delete(client.Annotations, pushAppAnnotationName) + delete(client.Annotations, upsUrlAnnotationName) + delete(client.Annotations, extVariantAnnotationName) + + _, err = helper.mobileclient.MobileV1alpha1().MobileClients(os.Getenv(constants.EnvVarKeyNamespace)).Update(client) + if err != nil { + log.Printf("Unable to update mobile client %s. Error: %s", clientId, err.Error()) + } + } + +} + +type variantAnnotationConfig struct { + Type string `json:"type"` + TypeLabel string `json:"typeLabel"` + Url string `json:"url"` + Id string `json:"id"` +} + +func getVariantTypeLabel(variantType string) string { + if strings.EqualFold(variantType, "android") { + return "Android" + } else if strings.EqualFold(variantType, "ios") { + return "iOS" + } else { + return variantType + } +} diff --git a/pkg/configOperator/configOperator.go b/pkg/configOperator/configOperator.go new file mode 100644 index 0000000..5b94118 --- /dev/null +++ b/pkg/configOperator/configOperator.go @@ -0,0 +1,389 @@ +package configOperator + +import ( + "strconv" + + "encoding/json" + + "log" + + "github.com/satori/go.uuid" + "k8s.io/apimachinery/pkg/runtime" + "fmt" + "strings" + "time" + + "k8s.io/client-go/pkg/api/v1" + "github.com/aerogear/ups-config-operator/pkg/constants" +) + +var letters = []rune("abcdefghijklmnopqrstuvwxyz0123456789") + +type ConfigOperator struct { + pushClient UpsClient + annotationHelper AnnotationHelper + kubeHelper KubeHelper +} + +func NewConfigOperator(pushClient UpsClient, annotationHelper AnnotationHelper, kubeHelper KubeHelper) *ConfigOperator { + op := new(ConfigOperator) + + op.pushClient = pushClient + op.annotationHelper = annotationHelper + op.kubeHelper = kubeHelper + + return op +} + +func (op ConfigOperator) StartService() { + log.Print("Entering watch loop") + + go op.startPollingUPS() + op.startKubeWatchLoop() +} + +// startPollingUPS() is a loop that calls compareUPSVariantsWithClientConfigs() in intervals +func (op ConfigOperator) startPollingUPS() { + interval := constants.UPSPollingInterval * time.Second + for { + <-time.After(interval) + op.compareUPSVariantsWithClientConfigs() + } +} + +func (op ConfigOperator) startKubeWatchLoop() { + events, err := op.kubeHelper.startSecretWatch() + if err != nil { + panic(err.Error()) + } + + for update := range events.ResultChan() { + switch action := update.Type; action { + case constants.K8SecretEventTypeAdded: + op.handleAddSecret(update.Object) + case constants.K8SecretEventTypeDeleted: + op.handleDeleteSecret(update.Object) + default: + log.Print("Unhandled action:", action) + } + } +} + +func (op ConfigOperator) handleAddSecret(obj runtime.Object) { + raw, _ := json.Marshal(obj) + var secret = BindingSecret{} + json.Unmarshal(raw, &secret) + if val, ok := secret.Labels[constants.SecretTypeLabelKey]; ok && val == constants.BindingSecretTypeMobile { + appType := string(secret.Data[constants.BindingDataAppTypeKey]) + log.Printf("A mobile binding secret of type `%s` was added", appType) + + if appType == "Android" { + op.handleAndroidVariant(&secret) + } else if appType == "IOS" { + op.handleIOSVariant(&secret) + } + // Always delete the secret after handling it regardless of any new resources + // was created + op.kubeHelper.deleteSecret(secret.Name) + } +} + +func (op ConfigOperator) handleDeleteSecret(obj runtime.Object) { + raw, _ := json.Marshal(obj) + var secret = BindingSecret{} + json.Unmarshal(raw, &secret) + + for _, ref := range secret.ObjectMeta.OwnerReferences { + if ref.Kind == "ServiceBinding" { + op.handleDeleteVariant(&secret) + break + } + } +} + +// compareUPSVariantsWithClientConfigs() compares the UPS client configs stored in k8's secrets +// against the variants in UPS in order to detect if a variant has been deleted in UPS +// If a client config is found that references a variant not found in UPS then we clean up the client config by deleting the associated servicebinding. +func (op ConfigOperator) compareUPSVariantsWithClientConfigs() { + // get the UPS related secrets + selector := fmt.Sprintf("serviceName=ups,pushApplicationId=%s", op.pushClient.getApplicationId()) + secretsList, err := op.kubeHelper.listSecrets(selector) + secrets:= secretsList.Items + + if err != nil { + log.Printf("Error searching for ups secrets: %v", err.Error()) + return + } + + // process the secrets into a list of VariantServiceBindingMappings + // each element has VariantId and ServiceBindingId + clientConfigs := op.getUPSVariantServiceBindingMappings(secrets) + + // Get all variants from UPS + UPSVariants, err := op.pushClient.getVariants() + + if err != nil { + log.Printf("An error occurred trying to get variants from UPS service: %v", err.Error()) + return + } + + for _, clientConfig := range clientConfigs { + found := false + + for _, variant := range UPSVariants { + if variant.VariantID == clientConfig.VariantId { + found = true + break + } + } + + if !found { + fmt.Printf("variant Id %v found in client configs but not found in UPS. Should delete", clientConfig.VariantId) + err := op.handleDeleteServiceBinding(clientConfig.ServiceBindingId) + if err != nil { + log.Printf("Error deleting service binding instance with id %s\n%s", clientConfig.ServiceBindingId, err.Error()) + } + } + } +} + +// getUPSVariantServiceBindingMappings() takes the list of secrets and returns a list of VariantServiceBindingMappings +func (op ConfigOperator) getUPSVariantServiceBindingMappings(secrets []v1.Secret) []VariantServiceBindingMapping { + + var results []VariantServiceBindingMapping + + buildAndAppendResult := func(results []VariantServiceBindingMapping, variantId string, serviceBindingId string, secret v1.Secret) []VariantServiceBindingMapping { + if variantServiceBindingMapping, err := GetClientConfigRepresentation(variantId, serviceBindingId); err != nil { + log.Printf("invalid android UPS client config found in secret %s reason: %s", secret.Name, err.Error()) + return results + } else { + return append(results, variantServiceBindingMapping) + } + } + + for _, secret := range secrets { + + // Retrieve the current config as an object + clientConfig := UPSClientConfig{} + json.Unmarshal(secret.Data["config"], &clientConfig) + + if clientConfig.Android != nil { + androidConfig := *clientConfig.Android + variantId := androidConfig["variantId"] + serviceBindingId := secret.ObjectMeta.Annotations["binding/android"] + results = buildAndAppendResult(results, variantId, serviceBindingId, secret) + } + + if clientConfig.IOS != nil { + iOSConfig := *clientConfig.IOS + variantId := iOSConfig["variantId"] + serviceBindingId := secret.ObjectMeta.Annotations["binding/ios"] + results = buildAndAppendResult(results, variantId, serviceBindingId, secret) + } + } + return results +} + +func (op ConfigOperator) handleDeleteServiceBinding(servicebindingId string) error { + serviceBindingName, err := op.kubeHelper.getServiceBindingNameByID(servicebindingId) + if err != nil { + return err + } + err = op.kubeHelper.deleteServiceBinding(serviceBindingName) + return err +} + +func (op ConfigOperator) handleAndroidVariant(secret *BindingSecret) { + clientId := string(secret.Data[constants.BindingDataClientIdKey]) + googleKey := string(secret.Data[constants.BindingDataGoogleKey]) + projectNumber := string(secret.Data[constants.BindingDataProjectNumberKey]) + serviceBindingId := string(secret.Data[constants.BindingDataServiceBindingIdKey]) + serviceInstanceName := string(secret.Data[constants.BindingDataServiceInstanceNameKey]) + + payload := &AndroidVariant{ + ProjectNumber: projectNumber, + GoogleKey: googleKey, + Variant: Variant{ + Name: clientId, + VariantID: uuid.NewV4().String(), + Secret: uuid.NewV4().String(), + }, + } + + log.Print("Creating a new android variant", payload) + success, variant := op.pushClient.createAndroidVariant(payload) + if success { + config, _ := variant.getJson() + op.updateConfiguration("android", clientId, variant.VariantID, config, serviceBindingId, serviceInstanceName) + } else { + log.Println("No variant has been created in UPS, skipping config secret") + } +} + +func (op ConfigOperator) handleIOSVariant(secret *BindingSecret) { + clientId := string(secret.Data[constants.BindingDataClientIdKey]) + cert := string(secret.Data[constants.BindingDataIOSCertKey]) + passPhrase := string(secret.Data[constants.BindingDataIOSPassPhraseKey]) + serviceBindingId := string(secret.Data[constants.BindingDataServiceBindingIdKey]) + serviceInstanceName := string(secret.Data[constants.BindingDataServiceInstanceNameKey]) + isProductionString := string(secret.Data[constants.BindingDataIOSIsProductionKey]) + isProduction, err := strconv.ParseBool(isProductionString) + + if err != nil { + log.Printf("iOS variant with clientId %v is invalid, isProduction value %v should be true or false. Setting to false", clientId, isProductionString) + isProduction = false + } + + certByteArray := []byte(cert) + payload := &IOSVariant{ + Certificate: certByteArray, + Passphrase: passPhrase, + Production: isProduction, //false for now while testing functionality + Variant: Variant{ + Name: clientId, + VariantID: uuid.NewV4().String(), + Secret: uuid.NewV4().String(), + }, + } + + success, variant := op.pushClient.createIOSVariant(payload) + if success { + config, _ := variant.getJson() + op.updateConfiguration("ios", clientId, variant.VariantID, config, serviceBindingId, serviceInstanceName) + } else { + log.Print("No variant has been created in UPS, skipping config secret") + } +} + +// Deletes a configuration from the config secret and from the UPS server +func (op ConfigOperator) handleDeleteVariant(secret *BindingSecret) { + appType := strings.ToLower(string(secret.Data["appType"])) + success, variantId := op.removeConfigFromClientSecret(secret, appType) + + if success { + success := op.pushClient.deleteVariant(appType, variantId) + if !success { + log.Printf("UPS reported an error when deleting variant %s", variantId) + } + } +} + +// Removes a platform configuration (e.g. iOS or Android) from the `Data.config` map of a UPS configuration +// secret. If there is only one platform it will delete the whole secret. +func (op ConfigOperator) removeConfigFromClientSecret(secret *BindingSecret, appType string) (bool, string) { + clientId := string(secret.Data["clientId"]) + + if clientId == "" { + // this secret is not the secret we're looking for + return false, "" + } + + configSecret := op.kubeHelper.findMobileClientConfig(clientId) + + if configSecret == nil { + log.Printf("Cannot delete configuration for client `%s` because the secret does not exist", clientId) + return false, "" + } + + serviceInstanceName := string(configSecret.Data[constants.BindingDataServiceInstanceNameKey]) + log.Printf("Deleting %s configuration from %s", appType, clientId) + + // Remove the annotation also from the mobile client + op.annotationHelper.removeAnnotationFromMobileClient(clientId, appType, serviceInstanceName) + + // Get the current config + // Retrieve the current config as an object + var currentConfig map[string]json.RawMessage + json.Unmarshal(configSecret.Data["config"], ¤tConfig) + + // Get the variant ID before removing the config + // We need that to delete the variant in UPS + variantId := op.getVariantIdFromConfig(string(currentConfig[appType])) + + // If there is only one platform in the configuration we can remove the whole + // secret + if len(currentConfig) == 1 { + op.kubeHelper.deleteSecret(configSecret.Name) + return true, variantId + } else { + log.Println("More than one variant available, updating configuration object") + + // Delete the config of the given app type and it's annotations + delete(currentConfig, appType) + delete(configSecret.Annotations, fmt.Sprintf("binding/%s", appType)) + + // Create a string of the new config object + currentConfigString, err := json.Marshal(currentConfig) + if err != nil { + panic(err.Error()) + } + + configSecret.Data["config"] = currentConfigString + _, err = op.kubeHelper.updateSecret(configSecret) + if err != nil { + log.Println(err.Error()) + } + + return true, variantId + } +} + +func (op ConfigOperator) getVariantIdFromConfig(config string) string { + configMap := make(map[string]string) + json.Unmarshal([]byte(config), &configMap) + return configMap["variantId"] +} + +// Updates the `Data.config` map of a UPS configuration secret +// The secret can contain multiple variants (e.g. iOS and Android) but is bound to one mobile client +func (op ConfigOperator) updateConfiguration(appType string, clientId string, variantId string, newConfig []byte, bindingId string, serviceInstanceName string) { + configSecret := op.kubeHelper.findMobileClientConfig(clientId) + if configSecret == nil { + // No config secret exists for this client yet. Create one. + configSecret = op.kubeHelper.createClientConfigSecret(clientId, serviceInstanceName, op.pushClient.getServiceInstanceId(), op.pushClient.getApplicationId()) + } + + // Retrieve the current config as an object + var currentConfig map[string]json.RawMessage + json.Unmarshal(configSecret.Data["config"], ¤tConfig) + + // Overwrite the old platform config + currentConfig[appType] = []byte(newConfig) + + // Create a string of the complete config object + currentConfigString, err := json.Marshal(currentConfig) + if err != nil { + panic(err.Error()) + } + + // Set the new config + configSecret.Data["uri"] = []byte(op.pushClient.getBaseUrl()) + configSecret.Data["config"] = currentConfigString + configSecret.Data["name"] = []byte("ups") + configSecret.Data["type"] = []byte("push") + + // Add the binding annotation to the UPS secret: this is done to link the actual ServiceBinding + // Instance back to this secret. In case the variant is deleted in UPS we can use this ID to delete + // the service binding + bindingAnnotation := fmt.Sprintf("binding/%s", appType) + if configSecret.Annotations == nil { + configSecret.Annotations = make(map[string]string) + } + configSecret.Annotations[bindingAnnotation] = bindingId + + pushApplicationName, err := op.pushClient.getPushApplicationName() + if err != nil { + // don't fail because of name not fetched. just use the id as the name + pushApplicationName = op.pushClient.getApplicationId() + } + + log.Println("Adding annotations to mobile client") + op.annotationHelper.addAnnotationToMobileClient(clientId, op.pushClient.getBaseUrl(), op.pushClient.getApplicationId(), pushApplicationName, appType, variantId, serviceInstanceName) + + _, err = op.kubeHelper.updateSecret(configSecret) + if err != nil { + log.Println(err.Error()) + } else { + log.Printf("%s configuration of %s has been updated", appType, clientId) + } +} diff --git a/pkg/configOperator/configOperator_test.go b/pkg/configOperator/configOperator_test.go new file mode 100644 index 0000000..0c049e6 --- /dev/null +++ b/pkg/configOperator/configOperator_test.go @@ -0,0 +1,321 @@ +package configOperator + +import ( + "testing" + + "k8s.io/client-go/pkg/api/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/stretchr/testify/mock" + "reflect" +) + +var op *ConfigOperator; + +var pushClient *MockUpsClient +var annotationHelper *MockAnnotationHelper +var kubeHelper *MockKubeHelper + +func setup() { + pushClient = new(MockUpsClient) + annotationHelper = new(MockAnnotationHelper) + kubeHelper = new(MockKubeHelper) + + op = NewConfigOperator(pushClient, annotationHelper, kubeHelper) +} + +func TestConfigOperator_compareUPSVariantsWithClientConfigs(t *testing.T) { + setup() + + // create secret list + secretData1 := map[string][]byte{ + "config": []byte("{\"Android\":{\"variantId\":\"foo\"}}"), + } + objectMeta1 := metav1.ObjectMeta{ + Annotations: map[string]string{ + "binding/android": "toBeKept", + }, + } + + secretData2 := map[string][]byte{ + "config": []byte("{\"IOS\":{\"variantId\":\"bar\"}}"), + } + objectMeta2 := metav1.ObjectMeta{ + Annotations: map[string]string{ + "binding/ios": "toBeDeleted", + }, + } + + secretList := &v1.SecretList{Items: []v1.Secret{ + {Data: secretData1, ObjectMeta: objectMeta1}, + {Data: secretData2, ObjectMeta: objectMeta2}}, + } + + // create variant list + variantList := []Variant{ + {VariantID: "foo"}, + } + + pushClient.On("getApplicationId").Return("myapp") + kubeHelper.On("listSecrets", "serviceName=ups,pushApplicationId=myapp").Return(secretList, nil) + pushClient.On("getVariants").Return(variantList, nil) + kubeHelper.On("getServiceBindingNameByID", "toBeDeleted").Return("nameOfTheServiceBindingToDelete", nil) + kubeHelper.On("deleteServiceBinding", "nameOfTheServiceBindingToDelete").Return(nil) + + op.compareUPSVariantsWithClientConfigs() + + kubeHelper.AssertExpectations(t) +} + +func TestConfigOperator_handleDeleteSecret_whenThereAre2Variants(t *testing.T) { + setup() + + bindingSecret := BindingSecret{ + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{ + {Kind: "ServiceBinding"}, + {Kind: "SomethingElse"}, + }, + }, + Data: map[string][]byte{ + "appType": []byte("ANdroId"), + "clientId": []byte("myClientId"), + }, + } + + configSecret := &v1.Secret{ + Data: map[string][]byte{ + "serviceInstanceName": []byte("myServiceInstanceName"), + "config": []byte("{\"android\":{\"variantId\":\"myVariantId\", \"foo\":\"bar\"}, \"ios\":{\"variantId\":\"yourVariantId\",\"pop\":\"cake\"}}"), + }, + } + + configSecret.Annotations = map[string]string{ + "binding/android": "toBeGone", + "binding/ios": "toBeKept", + } + + kubeHelper.On("findMobileClientConfig", "myClientId").Return(configSecret) + annotationHelper.On("removeAnnotationFromMobileClient", "myClientId", "android", "myServiceInstanceName").Once() + kubeHelper.On("updateSecret", mock.Anything).Return(nil, nil) + pushClient.On("deleteVariant", "android", "myVariantId").Return(true) + + op.handleDeleteSecret(&bindingSecret) + + kubeHelper.AssertCalled(t, "updateSecret", mock.MatchedBy(func(secret *v1.Secret) bool { + // Annotation for Android should be deleted + annotationGood := reflect.DeepEqual(secret.Annotations, map[string]string{ + "binding/ios": "toBeKept", + }) + + // config for Android should be deleted + secretConfigGood := string(secret.Data["config"]) == "{\"ios\":{\"variantId\":\"yourVariantId\",\"pop\":\"cake\"}}" + + return annotationGood && secretConfigGood + })) + + kubeHelper.AssertNotCalled(t, "deleteSecret", mock.Anything) +} + +func TestConfigOperator_handleDeleteSecret_whenThereIs1Variant(t *testing.T) { + setup() + + bindingSecret := BindingSecret{ + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{ + {Kind: "ServiceBinding"}, + {Kind: "SomethingElse"}, + }, + }, + Data: map[string][]byte{ + "appType": []byte("ANdroId"), + "clientId": []byte("myClientId"), + }, + } + + configSecret := &v1.Secret{ + Data: map[string][]byte{ + "serviceInstanceName": []byte("myServiceInstanceName"), + "config": []byte("{\"android\":{\"variantId\":\"myVariantId\", \"foo\":\"bar\"}}"), + }, + } + + configSecret.Name = "mySecretName" + + configSecret.Annotations = map[string]string{ + "binding/android": "toBeGone", + } + + kubeHelper.On("findMobileClientConfig", "myClientId").Return(configSecret) + annotationHelper.On("removeAnnotationFromMobileClient", "myClientId", "android", "myServiceInstanceName").Once() + kubeHelper.On("deleteSecret", "mySecretName").Once() + pushClient.On("deleteVariant", "android", "myVariantId").Return(true) + + op.handleDeleteSecret(&bindingSecret) + + kubeHelper.AssertCalled(t, "deleteSecret", "mySecretName") + kubeHelper.AssertNotCalled(t, "updateSecret", mock.Anything) +} + +func TestConfigOperator_handleAddSecret_whenAndroid_andNoVariantExistsWithSameGoogleKey(t *testing.T) { + setup() + + bindingSecret := BindingSecret{ + Data: map[string][]byte{ + "appType": []byte("Android"), + "clientId": []byte("myClientId"), + "googleKey": []byte("myGoogleKey"), + "projectNumber": []byte("myProjectNumber"), + "serviceBindingId": []byte("myServiceBindingId"), + "serviceInstanceName": []byte("myServiceInstanceName"), + }, + } + bindingSecret.Labels = map[string]string{ + "secretType": "mobile-client-binding-secret", + } + bindingSecret.Name = "myBindingSecret" + + pushClient.On("getServiceInstanceId").Return("myPushServiceInstanceId") + pushClient.On("getApplicationId").Return("myPushApplicationId") + pushClient.On("getBaseUrl").Return("http://example.org") + pushClient.On("hasAndroidVariant", "myGoogleKey").Return(nil) + pushClient.On("getPushApplicationName").Return("myPushAppName", nil) + pushClient.On("createAndroidVariant", mock.Anything).Return(true, &AndroidVariant{ + ProjectNumber: "myProjectNumber", + GoogleKey: "myGoogleKey", + Variant: Variant{ + Name: "myAndroidVariant", + VariantID: "myVariantId", + Secret: "myVariantSecret", + }, + }) + + // no existing client config + kubeHelper.On("findMobileClientConfig", "myClientId").Return(nil) + + configSecret := &v1.Secret{ + Data: map[string][]byte{ + "serviceInstanceName": []byte("myServiceInstanceName"), + "config": []byte("{\"ios\":{\"variantId\":\"yourVariantId\",\"pop\":\"cake\"}}"), + }, + } + configSecret.Name = "mySecretName" + configSecret.Annotations = map[string]string{ + "binding/ios": "toBeKept", + } + + kubeHelper.On("createClientConfigSecret", "myClientId", "myServiceInstanceName", "myPushServiceInstanceId", "myPushApplicationId").Return(configSecret) + annotationHelper.On("addAnnotationToMobileClient", "myClientId", "http://example.org", "myPushApplicationId", "myPushAppName", "android", "myVariantId", "myServiceInstanceName").Once() + kubeHelper.On("updateSecret", mock.Anything).Return(nil, nil) + kubeHelper.On("deleteSecret", "myBindingSecret").Once() + + op.handleAddSecret(&bindingSecret) + + kubeHelper.AssertCalled(t, "updateSecret", mock.MatchedBy(func(secret *v1.Secret) bool { + // Annotation for Android should be deleted + if !reflect.DeepEqual(secret.Annotations, map[string]string{ + "binding/android": "myServiceBindingId", + "binding/ios": "toBeKept", + }) { + return false + } + + if string(secret.Data["uri"]) != "http://example.org" || + string(secret.Data["name"]) != "ups" || + string(secret.Data["type"]) != "push" { + return false; + } + + if string(secret.Data["config"]) != "{\"android\":{\"senderId\":\"myProjectNumber\",\"variantId\":\"myVariantId\",\"variantSecret\":\"myVariantSecret\"},\"ios\":{\"variantId\":\"yourVariantId\",\"pop\":\"cake\"}}" { + return false; + } + + return true + })) + + kubeHelper.AssertCalled(t, "deleteSecret", "myBindingSecret") + + kubeHelper.AssertExpectations(t) + annotationHelper.AssertExpectations(t) +} + + +func TestConfigOperator_handleAddSecret_whenIOS(t *testing.T) { + setup() + + bindingSecret := BindingSecret{ + Data: map[string][]byte{ + "appType": []byte("IOS"), + "clientId": []byte("myClientId"), + "googleKey": []byte("myGoogleKey"), + "projectNumber": []byte("myProjectNumber"), + "serviceBindingId": []byte("myServiceBindingId"), + "serviceInstanceName": []byte("myServiceInstanceName"), + }, + } + bindingSecret.Labels = map[string]string{ + "secretType": "mobile-client-binding-secret", + } + bindingSecret.Name = "myBindingSecret" + + pushClient.On("getServiceInstanceId").Return("myPushServiceInstanceId") + pushClient.On("getApplicationId").Return("myPushApplicationId") + pushClient.On("getBaseUrl").Return("http://example.org") + pushClient.On("createIOSVariant", mock.Anything).Return(true, &IOSVariant{ + Certificate: []byte("myCertificate"), + Passphrase: "myPassphrase", + Variant: Variant{ + Name: "myIOSVariant", + VariantID: "myVariantId", + Secret: "myVariantSecret", + }, + }) + pushClient.On("getPushApplicationName").Return("myPushAppName", nil) + + // no existing client config + kubeHelper.On("findMobileClientConfig", "myClientId").Return(nil) + + configSecret := &v1.Secret{ + Data: map[string][]byte{ + "serviceInstanceName": []byte("myServiceInstanceName"), + "config": []byte("{\"android\":{\"variantId\":\"yourVariantId\",\"pop\":\"cake\"}}"), + }, + } + configSecret.Name = "mySecretName" + configSecret.Annotations = map[string]string{ + "binding/android": "toBeKept", + } + + kubeHelper.On("createClientConfigSecret", "myClientId", "myServiceInstanceName", "myPushServiceInstanceId", "myPushApplicationId").Return(configSecret) + annotationHelper.On("addAnnotationToMobileClient", "myClientId", "http://example.org", "myPushApplicationId", "myPushAppName", "ios", "myVariantId", "myServiceInstanceName").Once() + kubeHelper.On("updateSecret", mock.Anything).Return(nil, nil) + kubeHelper.On("deleteSecret", "myBindingSecret").Once() + + op.handleAddSecret(&bindingSecret) + + kubeHelper.AssertCalled(t, "updateSecret", mock.MatchedBy(func(secret *v1.Secret) bool { + // Annotation for Android should be deleted + if !reflect.DeepEqual(secret.Annotations, map[string]string{ + "binding/android": "toBeKept", + "binding/ios": "myServiceBindingId", + }) { + return false + } + + if string(secret.Data["uri"]) != "http://example.org" || + string(secret.Data["name"]) != "ups" || + string(secret.Data["type"]) != "push" { + return false; + } + + if string(secret.Data["config"]) != "{\"android\":{\"variantId\":\"yourVariantId\",\"pop\":\"cake\"},\"ios\":{\"variantId\":\"myVariantId\",\"variantSecret\":\"myVariantSecret\"}}" { + return false; + } + + return true + })) + + kubeHelper.AssertCalled(t, "deleteSecret", "myBindingSecret") + + kubeHelper.AssertExpectations(t) + annotationHelper.AssertExpectations(t) +} diff --git a/pkg/configOperator/kubeHelper.go b/pkg/configOperator/kubeHelper.go new file mode 100644 index 0000000..7f057d8 --- /dev/null +++ b/pkg/configOperator/kubeHelper.go @@ -0,0 +1,160 @@ +package configOperator + +import ( + "github.com/aerogear/ups-config-operator/pkg/constants" + "fmt" + "os" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/pkg/api/v1" + "k8s.io/client-go/kubernetes" + "log" + "github.com/pkg/errors" + + sc "github.com/aerogear/mobile-crd-client/pkg/client/servicecatalog/clientset/versioned" + "math/rand" + "k8s.io/apimachinery/pkg/watch" +) + +type KubeHelper interface { + startSecretWatch() (watch.Interface, error) + listSecrets(selector string) (*v1.SecretList, error) + deleteSecret(name string) + getServiceBindingNameByID(bindingId string) (string, error) + findMobileClientConfig(clientId string) *v1.Secret + createClientConfigSecret(clientId string, serviceInstanceName string, serviceInstanceId string, pushAppId string) *v1.Secret + updateSecret(secret *v1.Secret) (*v1.Secret, error) + deleteServiceBinding(bindingName string) error +} + +type KubeHelperImpl struct { + k8client *kubernetes.Clientset + scclient *sc.Clientset +} + +func NewKubeHelper(k8client *kubernetes.Clientset, scclient *sc.Clientset) *KubeHelperImpl { + helper := new(KubeHelperImpl) + + helper.k8client = k8client + helper.scclient = scclient + + return helper +} + +func (helper KubeHelperImpl) startSecretWatch() (watch.Interface, error) { + return helper.k8client.CoreV1().Secrets(os.Getenv(constants.EnvVarKeyNamespace)).Watch(metav1.ListOptions{}) +} + +func (helper KubeHelperImpl) listSecrets(selector string) (*v1.SecretList, error) { + filter := metav1.ListOptions{LabelSelector: selector} + return helper.k8client.CoreV1().Secrets(os.Getenv(constants.EnvVarKeyNamespace)).List(filter) +} + +// Find a mobile client bound ups config secret +func (helper KubeHelperImpl) findMobileClientConfig(clientId string) *v1.Secret { + filter := metav1.ListOptions{LabelSelector: fmt.Sprintf("clientId=%s,serviceName=ups", clientId)} + secrets, err := helper.k8client.CoreV1().Secrets(os.Getenv(constants.EnvVarKeyNamespace)).List(filter) + + // TODO: remove error handling here! + if err != nil { + panic(err.Error()) + } + + // No secret exists yet, that's ok, we have to create one + if len(secrets.Items) == 0 { + return nil + } + + // TODO: remove error handling here! + // Multiple secrets for the same clientId found, that's an error + if len(secrets.Items) > 1 { + panic(fmt.Sprintf("Multiple secrets found for clientId %s", clientId)) + } + + return &secrets.Items[0] +} + +// Find a service binding by its ExternalID +func (helper KubeHelperImpl) getServiceBindingNameByID(bindingId string) (string, error) { + // Get a list of all service bindings in the namespace and find the one with a matching ExternalID + // This is not very efficient and could be improved with a jsonpath query but it looks like client-go + // does not support jsonpath or at least I could not find any examples. + bindings, err := helper.scclient.ServicecatalogV1beta1().ServiceBindings(os.Getenv(constants.EnvVarKeyNamespace)).List(metav1.ListOptions{}) + if err != nil { + return "", err + } + + for _, binding := range bindings.Items { + log.Printf("Checking service binding %s", binding.Name) + if binding.Spec.ExternalID == bindingId { + return binding.Name, nil + } + } + + return "", errors.New(fmt.Sprintf("Can't find a binding with ExternalID %s", bindingId)) +} + +func (helper KubeHelperImpl) deleteServiceBinding(bindingName string) error { + return helper.scclient.ServicecatalogV1beta1().ServiceBindings(os.Getenv(constants.EnvVarKeyNamespace)).Delete(bindingName, nil) +} + +// Creates a mobile client bound ups config secret +func (helper KubeHelperImpl) createClientConfigSecret(clientId string, serviceInstanceName string, serviceInstanceId string, pushAppId string) *v1.Secret { + configSecretName := fmt.Sprintf("ups-secret-%s-%s", clientId, getRandomIdentifier(5)) + + payload := v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: configSecretName, + Labels: map[string]string{ + "mobile": "enabled", + "serviceName": "ups", + + // Used by the mobile-cli to discover config objects + "serviceInstanceId": serviceInstanceId, + "clientId": clientId, + "pushApplicationId": pushAppId, + }, + }, + Data: map[string][]byte{ + // Used to generate the name of the UI annotations + constants.BindingDataServiceInstanceNameKey: []byte(serviceInstanceName), + "config": []byte("{}"), + }, + } + + secret, err := helper.k8client.CoreV1().Secrets(os.Getenv(constants.EnvVarKeyNamespace)).Create(&payload) + + // TODO: remove error handling here! + if err != nil { + log.Fatal("Error creating ups config secret", err) + } else { + log.Printf("Config secret `%s` for variant created", configSecretName) + } + + return secret +} + +func (helper KubeHelperImpl) updateSecret(secret *v1.Secret) (*v1.Secret, error) { + return helper.k8client.CoreV1().Secrets(os.Getenv(constants.EnvVarKeyNamespace)).Update(secret) +} + +// Deletes a secret +func (helper KubeHelperImpl) deleteSecret(name string) { + err := helper.k8client.CoreV1().Secrets(os.Getenv(constants.EnvVarKeyNamespace)).Delete(name, nil) + + // TODO: remove error handling here! + if err != nil { + log.Print("Error deleting secret", err) + } else { + log.Printf("Secret `%s` has been deleted", name) + } +} + +// Create a random identifier of the given length. Useful for randomized resource names +func getRandomIdentifier(length int) string { + result := make([]rune, length) + for i := 0; i < length; i++ { + result[i] = letters[rand.Intn(len(letters))] + } + + return string(result) +} diff --git a/pkg/configOperator/mock_AnnotationHelper.go b/pkg/configOperator/mock_AnnotationHelper.go new file mode 100644 index 0000000..e4b3bbd --- /dev/null +++ b/pkg/configOperator/mock_AnnotationHelper.go @@ -0,0 +1,19 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. +package configOperator + +import mock "github.com/stretchr/testify/mock" + +// MockAnnotationHelper is an autogenerated mock type for the AnnotationHelper type +type MockAnnotationHelper struct { + mock.Mock +} + +// addAnnotationToMobileClient provides a mock function with given fields: clientId, upsUrl, pushApplicationId, pushApplicationName, appType, variantUrl, serviceInstanceName +func (_m *MockAnnotationHelper) addAnnotationToMobileClient(clientId string, upsUrl string, pushApplicationId string, pushApplicationName string, appType string, variantUrl string, serviceInstanceName string) { + _m.Called(clientId, upsUrl, pushApplicationId, pushApplicationName, appType, variantUrl, serviceInstanceName) +} + +// removeAnnotationFromMobileClient provides a mock function with given fields: clientId, appType, serviceInstanceName +func (_m *MockAnnotationHelper) removeAnnotationFromMobileClient(clientId string, appType string, serviceInstanceName string) { + _m.Called(clientId, appType, serviceInstanceName) +} diff --git a/pkg/configOperator/mock_KubeHelper.go b/pkg/configOperator/mock_KubeHelper.go new file mode 100644 index 0000000..2b531fd --- /dev/null +++ b/pkg/configOperator/mock_KubeHelper.go @@ -0,0 +1,152 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. +package configOperator + +import mock "github.com/stretchr/testify/mock" +import v1 "k8s.io/client-go/pkg/api/v1" +import watch "k8s.io/apimachinery/pkg/watch" + +// MockKubeHelper is an autogenerated mock type for the KubeHelper type +type MockKubeHelper struct { + mock.Mock +} + +// createClientConfigSecret provides a mock function with given fields: clientId, serviceInstanceName, serviceInstanceId, pushAppId +func (_m *MockKubeHelper) createClientConfigSecret(clientId string, serviceInstanceName string, serviceInstanceId string, pushAppId string) *v1.Secret { + ret := _m.Called(clientId, serviceInstanceName, serviceInstanceId, pushAppId) + + var r0 *v1.Secret + if rf, ok := ret.Get(0).(func(string, string, string, string) *v1.Secret); ok { + r0 = rf(clientId, serviceInstanceName, serviceInstanceId, pushAppId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.Secret) + } + } + + return r0 +} + +// deleteSecret provides a mock function with given fields: name +func (_m *MockKubeHelper) deleteSecret(name string) { + _m.Called(name) +} + +// deleteServiceBinding provides a mock function with given fields: bindingName +func (_m *MockKubeHelper) deleteServiceBinding(bindingName string) error { + ret := _m.Called(bindingName) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(bindingName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// findMobileClientConfig provides a mock function with given fields: clientId +func (_m *MockKubeHelper) findMobileClientConfig(clientId string) *v1.Secret { + ret := _m.Called(clientId) + + var r0 *v1.Secret + if rf, ok := ret.Get(0).(func(string) *v1.Secret); ok { + r0 = rf(clientId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.Secret) + } + } + + return r0 +} + +// getServiceBindingNameByID provides a mock function with given fields: bindingId +func (_m *MockKubeHelper) getServiceBindingNameByID(bindingId string) (string, error) { + ret := _m.Called(bindingId) + + var r0 string + if rf, ok := ret.Get(0).(func(string) string); ok { + r0 = rf(bindingId) + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(bindingId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// listSecrets provides a mock function with given fields: selector +func (_m *MockKubeHelper) listSecrets(selector string) (*v1.SecretList, error) { + ret := _m.Called(selector) + + var r0 *v1.SecretList + if rf, ok := ret.Get(0).(func(string) *v1.SecretList); ok { + r0 = rf(selector) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.SecretList) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(selector) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// startSecretWatch provides a mock function with given fields: +func (_m *MockKubeHelper) startSecretWatch() (watch.Interface, error) { + ret := _m.Called() + + var r0 watch.Interface + if rf, ok := ret.Get(0).(func() watch.Interface); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(watch.Interface) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// updateSecret provides a mock function with given fields: secret +func (_m *MockKubeHelper) updateSecret(secret *v1.Secret) (*v1.Secret, error) { + ret := _m.Called(secret) + + var r0 *v1.Secret + if rf, ok := ret.Get(0).(func(*v1.Secret) *v1.Secret); ok { + r0 = rf(secret) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.Secret) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*v1.Secret) error); ok { + r1 = rf(secret) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/pkg/configOperator/mock_UpsClient.go b/pkg/configOperator/mock_UpsClient.go new file mode 100644 index 0000000..12675a6 --- /dev/null +++ b/pkg/configOperator/mock_UpsClient.go @@ -0,0 +1,171 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. +package configOperator + +import mock "github.com/stretchr/testify/mock" + +// MockUpsClient is an autogenerated mock type for the UpsClient type +type MockUpsClient struct { + mock.Mock +} + +// createAndroidVariant provides a mock function with given fields: variant +func (_m *MockUpsClient) createAndroidVariant(variant *AndroidVariant) (bool, *AndroidVariant) { + ret := _m.Called(variant) + + var r0 bool + if rf, ok := ret.Get(0).(func(*AndroidVariant) bool); ok { + r0 = rf(variant) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 *AndroidVariant + if rf, ok := ret.Get(1).(func(*AndroidVariant) *AndroidVariant); ok { + r1 = rf(variant) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*AndroidVariant) + } + } + + return r0, r1 +} + +// createIOSVariant provides a mock function with given fields: variant +func (_m *MockUpsClient) createIOSVariant(variant *IOSVariant) (bool, *IOSVariant) { + ret := _m.Called(variant) + + var r0 bool + if rf, ok := ret.Get(0).(func(*IOSVariant) bool); ok { + r0 = rf(variant) + } else { + r0 = ret.Get(0).(bool) + } + + var r1 *IOSVariant + if rf, ok := ret.Get(1).(func(*IOSVariant) *IOSVariant); ok { + r1 = rf(variant) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*IOSVariant) + } + } + + return r0, r1 +} + +// deleteVariant provides a mock function with given fields: platform, variantId +func (_m *MockUpsClient) deleteVariant(platform string, variantId string) bool { + ret := _m.Called(platform, variantId) + + var r0 bool + if rf, ok := ret.Get(0).(func(string, string) bool); ok { + r0 = rf(platform, variantId) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// getApplicationId provides a mock function with given fields: +func (_m *MockUpsClient) getApplicationId() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// getBaseUrl provides a mock function with given fields: +func (_m *MockUpsClient) getBaseUrl() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// getPushApplicationName provides a mock function with given fields: +func (_m *MockUpsClient) getPushApplicationName() (string, error) { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// getServiceInstanceId provides a mock function with given fields: +func (_m *MockUpsClient) getServiceInstanceId() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// getVariants provides a mock function with given fields: +func (_m *MockUpsClient) getVariants() ([]Variant, error) { + ret := _m.Called() + + var r0 []Variant + if rf, ok := ret.Get(0).(func() []Variant); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Variant) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// hasAndroidVariant provides a mock function with given fields: key +func (_m *MockUpsClient) hasAndroidVariant(key string) *AndroidVariant { + ret := _m.Called(key) + + var r0 *AndroidVariant + if rf, ok := ret.Get(0).(func(string) *AndroidVariant); ok { + r0 = rf(key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*AndroidVariant) + } + } + + return r0 +} diff --git a/cmd/server/types.go b/pkg/configOperator/types.go similarity index 66% rename from cmd/server/types.go rename to pkg/configOperator/types.go index c01ff1e..2ef652c 100644 --- a/cmd/server/types.go +++ b/pkg/configOperator/types.go @@ -1,43 +1,48 @@ -package main +package configOperator import ( "bytes" "encoding/json" "github.com/pkg/errors" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -type variant struct { +// This is required because importing core/v1/Secret leads to a double import and redefinition +// of log_dir +type BindingSecret struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + Data map[string][]byte `json:"data,omitempty" protobuf:"bytes,2,rep,name=data"` + StringData map[string]string `json:"stringData,omitempty" protobuf:"bytes,4,rep,name=stringData"` +} + +type Variant struct { Name string `json:"name"` Description string `json:"description"` VariantID string `json:"variantID"` Secret string `json:"secret"` } -type androidVariant struct { +type AndroidVariant struct { ProjectNumber string `json:"projectNumber"` GoogleKey string `json:"googleKey"` - variant + Variant } -type iOSVariant struct { +type IOSVariant struct { Certificate []byte `json:"certificate"` Passphrase string `json:"passphrase"` Production bool `json:"production"` - variant + Variant } -type pushApplication struct { +type PushApplication struct { ApplicationId string `json:"applicationId"` } -type VariantAnnotation struct { - Label string `json:"label"` - Value string `json:"value"` - Type string `json:"type"` -} - -func (this *androidVariant) getJson() ([]byte, error) { +func (this *AndroidVariant) getJson() ([]byte, error) { config := map[string]string{ "senderId": this.ProjectNumber, "variantId": this.VariantID, @@ -51,7 +56,7 @@ func (this *androidVariant) getJson() ([]byte, error) { return buffer.Bytes(), err } -func (this *iOSVariant) getJson() ([]byte, error) { +func (this *IOSVariant) getJson() ([]byte, error) { config := map[string]string{ "variantId": this.VariantID, "variantSecret": this.Secret, @@ -65,8 +70,8 @@ func (this *iOSVariant) getJson() ([]byte, error) { } type UPSClientConfig struct { - Android *map[string]string `json:"android,omitempty"` - IOS *map[string]string `json:"ios,omitempty"` + Android *map[string]string `json:"android,omitempty"` + IOS *map[string]string `json:"ios,omitempty"` } type VariantServiceBindingMapping struct { diff --git a/cmd/server/upsClient.go b/pkg/configOperator/upsClient.go similarity index 58% rename from cmd/server/upsClient.go rename to pkg/configOperator/upsClient.go index d5eb021..a13a387 100644 --- a/cmd/server/upsClient.go +++ b/pkg/configOperator/upsClient.go @@ -1,4 +1,4 @@ -package main +package configOperator import ( "bytes" @@ -11,15 +11,62 @@ import ( "net/http" ) -type upsClient struct { - config *pushApplication +type UpsClient interface { + getPushApplicationName() (string, error) + getVariants() ([]Variant, error) + hasAndroidVariant(key string) *AndroidVariant + createAndroidVariant(variant *AndroidVariant) (bool, *AndroidVariant) + createIOSVariant(variant *IOSVariant) (bool, *IOSVariant) + deleteVariant(platform string, variantId string) bool + getApplicationId() string + getServiceInstanceId() string + getBaseUrl() string +} + +type UpsClientImpl struct { + config *PushApplication serviceInstanceId string baseUrl string } +func NewUpsClientImpl(config *PushApplication, serviceInstanceId string, baseUrl string) *UpsClientImpl { + client := new(UpsClientImpl) + + client.config = config + client.serviceInstanceId = serviceInstanceId + client.baseUrl = baseUrl + + return client +} + const BaseUrl = "http://localhost:8080/rest/applications" -func (client *upsClient) deleteVariant(platform string, variantId string) bool { +// fetches the push application name from the UPS system +func (client *UpsClientImpl) getPushApplicationName() (string, error) { + url := fmt.Sprintf("%s/%s", BaseUrl, client.config.ApplicationId) + log.Printf("UPS request: %s", url) + + resp, err := http.Get(url) + if err != nil { + return "", err + } + + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + + var pushAppInfo map[string]json.RawMessage + err = json.Unmarshal(body, &pushAppInfo) + if err != nil { + return "", err + } + + appNameWithQuotes := string(pushAppInfo["name"]) // --> e.g. "foo" + appName := appNameWithQuotes[1 : len(appNameWithQuotes)-1] // strip the quotes --> foo + + return appName, nil +} + +func (client *UpsClientImpl) deleteVariant(platform string, variantId string) bool { variant := client.hasVariant(platform, variantId) if variant != nil { @@ -28,7 +75,7 @@ func (client *upsClient) deleteVariant(platform string, variantId string) bool { url := fmt.Sprintf("%s/%s/%s/%s", BaseUrl, client.config.ApplicationId, platform, variant.VariantID) - log.Printf("UPS request", url) + log.Printf("UPS request: %s", url) req, err := http.NewRequest(http.MethodDelete, url, nil) @@ -47,29 +94,8 @@ func (client *upsClient) deleteVariant(platform string, variantId string) bool { return false } -// Find a Variant by its variant id -func (client *upsClient) hasVariant(platform string, variantId string) *variant { - variants, err := client.getVariantsForPlatform(platform) - - if err != nil { - log.Fatal(err) - - // Return true here to prevent creating a new variant when the - // request fails - return &variant{} - } - - for _, variant := range variants { - if variant.VariantID == variantId { - return &variant - } - } - - return nil -} - // Find an Android Variant by its Google Key -func (client *upsClient) hasAndroidVariant(key string) *androidVariant { +func (client *UpsClientImpl) hasAndroidVariant(key string) *AndroidVariant { variants, err := client.getAndroidVariants() if err != nil { @@ -77,7 +103,7 @@ func (client *upsClient) hasAndroidVariant(key string) *androidVariant { // Return true here to prevent creating a new variant when the // request fails - return &androidVariant{} + return &AndroidVariant{} } for _, variant := range variants { @@ -89,9 +115,9 @@ func (client *upsClient) hasAndroidVariant(key string) *androidVariant { return nil } -func (client *upsClient) createAndroidVariant(variant *androidVariant) (bool, *androidVariant) { +func (client *UpsClientImpl) createAndroidVariant(variant *AndroidVariant) (bool, *AndroidVariant) { url := fmt.Sprintf("%s/%s/android", BaseUrl, client.config.ApplicationId) - log.Printf("UPS request", url) + log.Printf("UPS request: %s", url) payload, err := json.Marshal(variant) if err != nil { @@ -110,19 +136,19 @@ func (client *upsClient) createAndroidVariant(variant *androidVariant) (bool, *a panic(err.Error()) } - log.Printf("UPS responded with status code ", resp.StatusCode) + log.Printf("UPS responded with status code : %s", string(resp.StatusCode)) defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) - var createdVariant androidVariant + var createdVariant AndroidVariant json.Unmarshal(body, &createdVariant) return resp.StatusCode == 201, &createdVariant } -func (client *upsClient) createIOSVariant(variant *iOSVariant) (bool, *iOSVariant) { +func (client *UpsClientImpl) createIOSVariant(variant *IOSVariant) (bool, *IOSVariant) { url := fmt.Sprintf("%s/%s/ios", BaseUrl, client.config.ApplicationId) - log.Printf("UPS request", url) + log.Printf("UPS request: %s", url) production := "true" if !variant.Production { @@ -166,72 +192,97 @@ func (client *upsClient) createIOSVariant(variant *iOSVariant) (bool, *iOSVarian panic(err.Error()) } - log.Printf("UPS responded with status code: %s ", resp.StatusCode) + log.Printf("UPS responded with status code: %s ", string(resp.StatusCode)) defer resp.Body.Close() b, _ := ioutil.ReadAll(resp.Body) - var createdVariant iOSVariant + var createdVariant IOSVariant json.Unmarshal(b, &createdVariant) return resp.StatusCode == 201, &createdVariant } -func (client *upsClient) getVariantsForPlatformRaw(platform string) ([]byte, error) { - url := fmt.Sprintf("%s/%s/%s", BaseUrl, client.config.ApplicationId, platform) - log.Printf("UPS request", url) +func (client *UpsClientImpl) getVariantsForPlatform(platform string) ([]Variant, error) { + variantBytes, err := client.getVariantsForPlatformRaw(platform) - resp, err := http.Get(url) if err != nil { return nil, err } - defer resp.Body.Close() - body, _ := ioutil.ReadAll(resp.Body) - return body, nil + variants := make([]Variant, 0) + json.Unmarshal(variantBytes, &variants) + + return variants, nil } -func (client *upsClient) getVariantsForPlatform(platform string) ([]variant, error) { - variantBytes, err := client.getVariantsForPlatformRaw(platform) +func (client *UpsClientImpl) getVariants() ([]Variant, error) { + + UPSIOSVariants, err := client.getVariantsForPlatform("ios") + UPSAndroidVariants, err := client.getVariantsForPlatform("android") if err != nil { return nil, err } - variants := make([]variant, 0) - json.Unmarshal(variantBytes, &variants) + variants := append(UPSAndroidVariants, UPSIOSVariants...) return variants, nil } -func (client *upsClient) getAndroidVariants() ([]androidVariant, error) { +func (client *UpsClientImpl) getApplicationId() string { + return client.config.ApplicationId +} + +func (client *UpsClientImpl) getServiceInstanceId() string { + return client.serviceInstanceId +} + +func (client *UpsClientImpl) getBaseUrl() string { + return client.baseUrl +} + +////////////////////////////////////// internal things ///////////////////////////////////// + +func (client *UpsClientImpl) getAndroidVariants() ([]AndroidVariant, error) { variantsBytes, err := client.getVariantsForPlatformRaw("android") if err != nil { return nil, err } - androidVariants := make([]androidVariant, 0) + androidVariants := make([]AndroidVariant, 0) json.Unmarshal(variantsBytes, &androidVariants) return androidVariants, nil } -func (client *upsClient) getIOSVariants() ([]iOSVariant, error) { - variantsBytes, err := client.getVariantsForPlatformRaw("ios") +// Find a Variant by its variant id +func (client *UpsClientImpl) hasVariant(platform string, variantId string) *Variant { + variants, err := client.getVariantsForPlatform(platform) + if err != nil { - return nil, err + log.Fatal(err) + + // Return true here to prevent creating a new variant when the + // request fails + return &Variant{} } - iOSVariants := make([]iOSVariant, 0) - json.Unmarshal(variantsBytes, &iOSVariants) - return iOSVariants, nil -} -func (client *upsClient) getVariants() ([]variant, error) { + for _, variant := range variants { + if variant.VariantID == variantId { + return &variant + } + } - UPSIOSVariants, err := client.getVariantsForPlatform("ios") - UPSAndroidVariants, err := client.getVariantsForPlatform("android") + return nil +} + +func (client *UpsClientImpl) getVariantsForPlatformRaw(platform string) ([]byte, error) { + url := fmt.Sprintf("%s/%s/%s", BaseUrl, client.config.ApplicationId, platform) + log.Printf("UPS request: %s", url) + resp, err := http.Get(url) if err != nil { return nil, err } - variants := append(UPSAndroidVariants, UPSIOSVariants...) - - return variants, nil + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + return body, nil } diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go new file mode 100644 index 0000000..b5c7d05 --- /dev/null +++ b/pkg/constants/constants.go @@ -0,0 +1,37 @@ +package constants + +const ( + EnvVarKeyNamespace = "NAMESPACE" + + K8SecretEventTypeAdded = "ADDED" + K8SecretEventTypeDeleted = "DELETED" + + // time in seconds + UPSPollingInterval = 10 + + UpsSecretName = "unified-push-server" + + UpsSecretDataUrlKey = "uri" + UpsSecretLabelServiceInstanceIdKey = "serviceInstanceID" + + SecretTypeLabelKey = "secretType" + + BindingSecretTypeMobile = "mobile-client-binding-secret" + + BindingDataServiceBindingIdKey = "serviceBindingId" + BindingDataServiceInstanceNameKey = "serviceInstanceName" + + BindingDataAppTypeKey = "appType" + BindingDataClientIdKey = "clientId" + + BindingDataGoogleKey = "googleKey" + BindingDataProjectNumberKey = "projectNumber" + + BindingDataIOSCertKey = "cert" + BindingDataIOSPassPhraseKey = "passphrase" + BindingDataIOSIsProductionKey = "isProduction" + + PushAppAnnotationNameFormat = "org.aerogear.binding.%s/push-application" + UpsUrlAnnotationNameFormat = "org.aerogear.binding.%s/ups-url" + ExtVariantsAnnotationNameFormat = "org.aerogear.binding-ext.%s/variants" +)