diff --git a/Dockerfile b/Dockerfile index a1c85fb2c888d6948d78b44e724971ae6c5635a6..37081217965a8e9c8db6db06e70abb2f2be32681 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,7 @@ COPY go.mod/ . RUN go get github.com/golang/protobuf/protoc-gen-go RUN mkdir -p /build/pkg/api/v1 RUN protoc --proto_path=/build/api/proto/v1 --proto_path=/build/third_party --go_out=plugins=grpc:/build/pkg/api/v1 config-service.proto +RUN CGO_ENABLED=0 GOOS=linux go test ./... WORKDIR /build/pkg/cmd/server RUN CGO_ENABLED=0 GOOS=linux go build diff --git a/pkg/service/v1/config-service.go b/pkg/service/v1/config-service.go index c4acaef6008046ac05ce34f2a442958e649e65ec..b1c776f70b1ebeb73727049ac77ed799be52a415 100644 --- a/pkg/service/v1/config-service.go +++ b/pkg/service/v1/config-service.go @@ -476,11 +476,14 @@ func (s *readinessServiceServer) CheckIfReady(ctx context.Context, req *v1.Insta } func (s *informationServiceServer) RetrieveServiceIp(ctx context.Context, req *v1.InstanceRequest) (*v1.InfoServiceResponse, error) { + + log.Printf("Entered RetrieveServiceIp method") + // check if the API version requested by client is supported by server if err := checkAPI(req.Api, apiVersion); err != nil { return nil, err } - + depl := req.Deployment //check if given k8s namespace exists @@ -488,17 +491,30 @@ func (s *informationServiceServer) RetrieveServiceIp(ctx context.Context, req *v if err != nil { return prepareInfoResponse(v1.Status_FAILED, namespaceNotFound, ""), err } - + + log.Printf("About to read service %s details from namespace %s", depl.Uid, depl.Namespace) + app, err := s.kubeAPI.CoreV1().Services(depl.Namespace).Get(depl.Uid, metav1.GetOptions{}) if err != nil { return prepareInfoResponse(v1.Status_FAILED, "Service not found!", ""), err } - - ip := app.Status.LoadBalancer.Ingress[0].IP; - - if ip != "" { - return prepareInfoResponse(v1.Status_OK, "", ip), nil + + if len(app.Status.LoadBalancer.Ingress) > 0 { + log.Printf("Found %d loadbalancer ingresse(s)", len(app.Status.LoadBalancer.Ingress)) + + ip := app.Status.LoadBalancer.Ingress[0].IP + + if ip != "" { + log.Printf("Found IP address. Will return %s", ip) + return prepareInfoResponse(v1.Status_OK, "", ip), err + } else { + log.Printf("IP adress not found") + return prepareInfoResponse(v1.Status_FAILED, "Ip not found!", ""), err + } + } else { - return prepareInfoResponse(v1.Status_FAILED, "Ip not found!", ""), nil + log.Printf("No loadbalancer ingresses found") + return prepareInfoResponse(v1.Status_FAILED, "Service ingress not found!", ""), err } -} \ No newline at end of file + +} diff --git a/pkg/service/v1/config-service_test.go b/pkg/service/v1/config-service_test.go index 9335427d142cc644bfa7625920a2be8942dd9607..4ed3c31a66d851ab84c9649ec9a60ebe91bc0f6c 100644 --- a/pkg/service/v1/config-service_test.go +++ b/pkg/service/v1/config-service_test.go @@ -6,7 +6,7 @@ import ( "github.com/xanzy/go-gitlab" extension "k8s.io/api/extensions/v1beta1" corev1 "k8s.io/api/core/v1" - v12 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "testing" testclient "k8s.io/client-go/kubernetes/fake" ) @@ -84,6 +84,77 @@ func TestReadinessServiceServer_CheckIfReady(t *testing.T) { } } +func TestInformationServiceServer_RetrieveServiceIp(t *testing.T) { + client := testclient.NewSimpleClientset() + server := NewInformationServiceServer(client) + + //Fail on API version check + res, err := server.RetrieveServiceIp(context.Background(), &illegal_req) + if err == nil || res != nil { + t.Fail() + } + + //Fail on namespace check + freq := v1.InstanceRequest{Api:apiVersion, Deployment:&fake_ns_inst} + res, err = server.RetrieveServiceIp(context.Background(), &freq) + if err == nil || res.Status != v1.Status_FAILED { + t.Fail() + } + + //create mock namespace + ns := corev1.Namespace{} + ns.Name = "test-namespace" + _, _ = client.CoreV1().Namespaces().Create(&ns) + + //Fail on loading services + res, err = server.RetrieveServiceIp(context.Background(), &req) + if err == nil || res.Status != v1.Status_FAILED || res.Message != "Service not found!" { + t.Fail() + } + + //create mock service without ingress + s1 := corev1.Service{} + s1.Name = "test-uid" + _, _ = client.CoreV1().Services("test-namespace").Create(&s1) + + //Fail on missing service ingress + res, err = server.RetrieveServiceIp(context.Background(), &req) + if err != nil || res.Status != v1.Status_FAILED || res.Message != "Service ingress not found!"{ + t.Fail() + } + + //create mock service with ingress but no IP + s2 := corev1.Service{} + s2.Name = "test-uid" + i1 := corev1.LoadBalancerIngress{} + ing := []corev1.LoadBalancerIngress{i1} + s2.Status.LoadBalancer.Ingress = ing + client.CoreV1().Services("test-namespace").Delete("test-uid", &metav1.DeleteOptions{}) + _, _ = client.CoreV1().Services("test-namespace").Create(&s2) + + //Fail on missing service ingress IP + res, err = server.RetrieveServiceIp(context.Background(), &req) + if err != nil || res.Status != v1.Status_FAILED || res.Message != "Ip not found!"{ + t.Fail() + } + + //create mock service with ingress and IP + s3 := corev1.Service{} + s3.Name = "test-uid" + i2 := corev1.LoadBalancerIngress{} + i2.IP = "10.10.1.1" + ing2 := []corev1.LoadBalancerIngress{i2} + s3.Status.LoadBalancer.Ingress = ing2 + client.CoreV1().Services("test-namespace").Delete("test-uid", &metav1.DeleteOptions{}) + _, _ = client.CoreV1().Services("test-namespace").Create(&s3) + + //Pass + res, err = server.RetrieveServiceIp(context.Background(), &req) + if res.Status != v1.Status_OK || res.Info != "10.10.1.1" { + t.Fail() + } +} + func TestCertManagerServiceServer_DeleteIfExists(t *testing.T) { client := testclient.NewSimpleClientset() server := NewCertManagerServiceServer(client) @@ -177,7 +248,7 @@ func TestBasicAuthServiceServer_CreateOrReplace(t *testing.T) { t.Fail() } - sec, err := client.CoreV1().Secrets("test-namespace").Get(getAuthSecretName("test-uid"), v12.GetOptions{}) + sec, err := client.CoreV1().Secrets("test-namespace").Get(getAuthSecretName("test-uid"), metav1.GetOptions{}) if err != nil || sec == nil { t.Fail() } @@ -227,4 +298,4 @@ func TestConfigServiceServer_DeleteIfExists(t *testing.T) { if err != nil || res.Status != v1.Status_OK { t.Fail() } -} \ No newline at end of file +} diff --git a/test_version/config-service.go b/test_version/config-service.go new file mode 100644 index 0000000000000000000000000000000000000000..084aaf34df37a292e43a12b2979c64be81d00a0f --- /dev/null +++ b/test_version/config-service.go @@ -0,0 +1,522 @@ +package v1 + +import ( + "context" + "encoding/base64" + "github.com/xanzy/go-gitlab" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + apiv1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "log" + "math/rand" + + "code.geant.net/stash/scm/nmaas/nmaas-janitor/pkg/api/v1" + "github.com/johnaoss/htpasswd/apr1" +) + +const ( + apiVersion = "v1" + namespaceNotFound = "Namespace not found" +) + +type configServiceServer struct { + kubeAPI kubernetes.Interface + gitAPI *gitlab.Client +} + +type basicAuthServiceServer struct { + kubeAPI kubernetes.Interface +} + +type certManagerServiceServer struct { + kubeAPI kubernetes.Interface +} + +type readinessServiceServer struct { + kubeAPI kubernetes.Interface +} + +type informationServiceServer struct { + kubeAPI kubernetes.Interface +} + +func NewConfigServiceServer(kubeAPI kubernetes.Interface, gitAPI *gitlab.Client) v1.ConfigServiceServer { + return &configServiceServer{kubeAPI: kubeAPI, gitAPI: gitAPI} +} + +func NewBasicAuthServiceServer(kubeAPI kubernetes.Interface) v1.BasicAuthServiceServer { + return &basicAuthServiceServer{kubeAPI: kubeAPI} +} + +func NewCertManagerServiceServer(kubeAPI kubernetes.Interface) v1.CertManagerServiceServer { + return &certManagerServiceServer{kubeAPI: kubeAPI} +} + +func NewReadinessServiceServer(kubeAPI kubernetes.Interface) v1.ReadinessServiceServer { + return &readinessServiceServer{kubeAPI: kubeAPI} +} + +func NewInformationServiceServer(kubeAPI kubernetes.Interface) v1.InformationServiceServer { + return &informationServiceServer{kubeAPI: kubeAPI} +} + +func checkAPI(api string, current string) error { + if len(api) > 0 && current != api { + return status.Errorf(codes.Unimplemented, + "unsupported API version: service implements API version '%s', but asked for '%s'", apiVersion, api) + } + return nil +} + +//Prepare response +func prepareResponse(status v1.Status, message string) *v1.ServiceResponse { + return &v1.ServiceResponse{ + Api: apiVersion, + Status: status, + Message: message, + } +} + +//Prepare info response +func prepareInfoResponse(status v1.Status, message string, info string) *v1.InfoServiceResponse { + return &v1.InfoServiceResponse{ + Api: apiVersion, + Status: status, + Message: message, + Info: info, + } +} + +//Find proper project, given user namespace and instance uid +func (s *configServiceServer) FindGitlabProjectId(api *gitlab.Client, uid string, domain string) (int, error) { + //Find exact group + groups, _, err := api.Groups.SearchGroup(domain) + if len(groups) != 1 || err != nil { + log.Printf("Found %d groups in domain %s", len(groups), domain) + log.Print(err) + return -1, status.Errorf(codes.NotFound, "Gitlab Group for given domain does not exist") + } + + //List group projects + projs, _, err := api.Groups.ListGroupProjects(groups[0].ID, nil) + if err != nil || len(projs) == 0 { + log.Printf("Group %s is empty or unaccessible", groups[0].Name) + return -1, status.Errorf(codes.NotFound, "Project containing config not found on Gitlab") + } + + //Find our project in group projects list + for _, proj := range projs { + if proj.Name == uid { + return proj.ID, nil + } + } + + return -1, status.Errorf(codes.NotFound, "Project containing config not found on Gitlab") +} + +//Parse repository files into kubernetes json data part for patching +func (s *configServiceServer) PrepareDataJsonFromRepository(api *gitlab.Client, repoId int) ([]byte, error) { + //List files + tree, _, err := api.Repositories.ListTree(repoId, nil) + if err != nil { + log.Print(err) + return nil, status.Errorf(codes.NotFound, "Cannot find any config files") + } + + numFiles := len(tree) + + //create helper strings + mapStart := []byte("{\"binaryData\": {\"") + mapAfterName := []byte("\": \"") + mapNextData := []byte("\", \"") + mapAfterLast := []byte("\"}}") + + //Start parsing + compiledMap := mapStart + for i, file := range tree { + if file.Type != "blob" { + continue + } + opt := &gitlab.GetRawFileOptions{Ref: gitlab.String("master")} + data, _, err := api.RepositoryFiles.GetRawFile(repoId, file.Name, opt) + if err != nil { + log.Print(err) + return nil, status.Errorf(codes.Internal, "Error while reading file from Gitlab!") + } + + compiledMap = append(compiledMap, file.Name...) + compiledMap = append(compiledMap, mapAfterName...) + compiledMap = append(compiledMap, base64.StdEncoding.EncodeToString(data)...) + + if numFiles-1 != i { //it's not last element + compiledMap = append(compiledMap, mapNextData...) + } + } + compiledMap = append(compiledMap, mapAfterLast...) + + return compiledMap, nil +} + +//Parse repository files into string:string map for configmap creator +func (s *configServiceServer) PrepareDataMapFromRepository(api *gitlab.Client, repoId int) (map[string][]byte, error) { + + compiledMap := make(map[string][]byte) + + //List files + tree, _, err := api.Repositories.ListTree(repoId, nil) + if err != nil { + log.Print(err) + return nil, status.Errorf(codes.NotFound, "Cannot find any config files") + } + //if len(tree) == 0 { + // log.Printf("There are no files to config in repo %d", repoId) + // return compiledMap, nil + //} + + //Start parsing + for _, file := range tree { + if file.Type != "blob" { + continue + } + + opt := &gitlab.GetRawFileOptions{Ref: gitlab.String("master")} + data, _, err := api.RepositoryFiles.GetRawFile(repoId, file.Name, opt) + if err != nil { + log.Print(err) + return nil, status.Errorf(codes.Internal, "Error while reading file from Gitlab!") + } + + //assign retrieved binary data to newly created configmap + compiledMap[file.Name] = data + } + + return compiledMap, nil +} + +//Create new configmap +func (s *configServiceServer) CreateOrReplace(ctx context.Context, req *v1.InstanceRequest) (*v1.ServiceResponse, error) { + // check if the API version requested by client is supported by server + if err := checkAPI(req.Api, apiVersion); err != nil { + return nil, err + } + + depl := req.Deployment + + proj, err := s.FindGitlabProjectId(s.gitAPI, depl.Uid, depl.Domain) + if err != nil { + return prepareResponse(v1.Status_FAILED, "Cannot find corresponding gitlab assets"), err + } + + //check if given k8s namespace exists + _, err = s.kubeAPI.CoreV1().Namespaces().Get(depl.Namespace, metav1.GetOptions{}) + if err != nil{ + ns := apiv1.Namespace{} + ns.Name = depl.Namespace + _, err = s.kubeAPI.CoreV1().Namespaces().Create(&ns) + if err != nil { + return prepareResponse(v1.Status_FAILED, namespaceNotFound), err + } + } + + //check if configmap already exists + _, err = s.kubeAPI.CoreV1().ConfigMaps(depl.Namespace).Get(depl.Uid, metav1.GetOptions{}) + if err != nil { //Not exists, we create new + cm := apiv1.ConfigMap{} + cm.SetName(depl.Uid) + cm.SetNamespace(depl.Namespace) + cm.BinaryData, err = s.PrepareDataMapFromRepository(s.gitAPI, proj) + if err != nil { + return prepareResponse(v1.Status_FAILED, "Failed to retrieve data from repository"), err + } + _, err = s.kubeAPI.CoreV1().ConfigMaps(depl.Namespace).Create(&cm) + if err != nil { + return prepareResponse(v1.Status_FAILED, "Failed to create ConfigMap"), err + } + + return prepareResponse(v1.Status_OK, "ConfigMap created successfully"), nil + } else { //Already exists, we patch it + data, err := s.PrepareDataJsonFromRepository(s.gitAPI, proj) + if err != nil { + return prepareResponse(v1.Status_FAILED, "Error while parsing configuration data"), err + } + + //patch configmap + _, err = s.kubeAPI.CoreV1().ConfigMaps(depl.Namespace).Patch(depl.Uid, types.MergePatchType, data) + if err != nil { + return prepareResponse(v1.Status_FAILED, "Error while patching configmap!"), err + } + + return prepareResponse(v1.Status_OK, "ConfigMap updated successfully"), nil + } +} + +//Delete configmap for instance +func (s *configServiceServer) DeleteIfExists(ctx context.Context, req *v1.InstanceRequest) (*v1.ServiceResponse, error) { + // check if the API version requested by client is supported by server + if err := checkAPI(req.Api, apiVersion); err != nil { + return nil, err + } + + depl := req.Deployment + + //check if given k8s namespace exists + _, err := s.kubeAPI.CoreV1().Namespaces().Get(depl.Namespace, metav1.GetOptions{}) + if err != nil { + return prepareResponse(v1.Status_FAILED, namespaceNotFound), err + } + + //check if configmap exist + _, err = s.kubeAPI.CoreV1().ConfigMaps(depl.Namespace).Get(depl.Uid, metav1.GetOptions{}) + if err != nil { + return prepareResponse(v1.Status_OK,"ConfigMap not exists or is unavailable"), nil + } + + //delete configmap + err = s.kubeAPI.CoreV1().ConfigMaps(depl.Namespace).Delete(depl.Uid, &metav1.DeleteOptions{}) + if err != nil { + return prepareResponse(v1.Status_FAILED, "Error while removing configmap!"), err + } + + return prepareResponse(v1.Status_OK, "ConfigMap deleted successfully"), nil +} + +func randomString(l int) string { + bytes := make([]byte, l) + for i := 0; i < l; i++ { + bytes[i] = byte(65 + rand.Intn(90-65)) + } + return string(bytes) +} + +func aprHashCredentials(user string, password string) (string, error) { + out, err := apr1.Hash(password, randomString(8)) + + if err != nil { + return "", status.Errorf(codes.Internal, "Failed to execute apr hashing") + } + + return user + ":" + out, nil +} + +func (s *basicAuthServiceServer) PrepareSecretDataFromCredentials(credentials *v1.Credentials) (map[string][]byte, error) { + hash, err := aprHashCredentials(credentials.User, credentials.Password) + if err != nil { + return nil, err + } + + resultMap := make(map[string][]byte) + resultMap["auth"] = []byte(hash) + + return resultMap, nil +} + +func (s *basicAuthServiceServer) PrepareSecretJsonFromCredentials(credentials *v1.Credentials) ([]byte, error) { + hash, err := aprHashCredentials(credentials.User, credentials.Password) + + if err != nil { + return nil, status.Errorf(codes.Internal, "Failed to execute htpasswd executable") + } + + result := []byte("{\"data\": {\"auth\": \"") + result = append(result, base64.StdEncoding.EncodeToString([]byte(hash))...) + result = append(result, "\"}}"...) + + return result, nil +} + +func getAuthSecretName(uid string) string { + return uid + "-auth" +} + +func (s *basicAuthServiceServer) CreateOrReplace(ctx context.Context, req *v1.InstanceCredentialsRequest) (*v1.ServiceResponse, error) { + // check if the API version requested by client is supported by server + if err := checkAPI(req.Api, apiVersion); err != nil { + return nil, err + } + + depl := req.Instance + + //check if given k8s namespace exists + _, err := s.kubeAPI.CoreV1().Namespaces().Get(depl.Namespace, metav1.GetOptions{}) + if err != nil{ + ns := apiv1.Namespace{} + ns.Name = depl.Namespace + _, err = s.kubeAPI.CoreV1().Namespaces().Create(&ns) + if err != nil { + return prepareResponse(v1.Status_FAILED, namespaceNotFound), err + } + } + + secretName := getAuthSecretName(depl.Uid) + + _, err = s.kubeAPI.CoreV1().Secrets(depl.Namespace).Get(secretName, metav1.GetOptions{}) + //Secret does not exist, we have to create it + if err != nil { + //create secret + secret := apiv1.Secret{} + secret.SetNamespace(depl.Namespace) + secret.SetName(secretName) + secret.Data, err = s.PrepareSecretDataFromCredentials(req.Credentials) + if err != nil { + return prepareResponse(v1.Status_FAILED, "Error while preparing secret!"), err + } + + //commit secret + _, err = s.kubeAPI.CoreV1().Secrets(depl.Namespace).Create(&secret) + if err != nil { + return prepareResponse(v1.Status_FAILED, "Error while creating secret!"), err + } + + return prepareResponse(v1.Status_OK, "Secret created successfully"), nil + } else { + patch, err := s.PrepareSecretJsonFromCredentials(req.Credentials) + if err != nil { + return prepareResponse(v1.Status_FAILED, "Error while parsing configuration data"), err + } + + //patch secret + _, err = s.kubeAPI.CoreV1().Secrets(depl.Namespace).Patch(secretName, types.MergePatchType, patch) + if err != nil { + return prepareResponse(v1.Status_FAILED, "Error while patching secret!"), err + } + + return prepareResponse(v1.Status_OK, "Secret updated successfully"), nil + } +} + +func (s *basicAuthServiceServer) DeleteIfExists(ctx context.Context, req *v1.InstanceRequest) (*v1.ServiceResponse, error) { + // check if the API version requested by client is supported by server + if err := checkAPI(req.Api, apiVersion); err != nil { + return nil, err + } + + depl := req.Deployment + + //check if given k8s namespace exists + _, err := s.kubeAPI.CoreV1().Namespaces().Get(depl.Namespace, metav1.GetOptions{}) + if err != nil { + return prepareResponse(v1.Status_FAILED, namespaceNotFound), err + } + + secretName := getAuthSecretName(depl.Uid) + + //check if secret exist + _, err = s.kubeAPI.CoreV1().Secrets(depl.Namespace).Get(secretName, metav1.GetOptions{}) + if err != nil { + return prepareResponse(v1.Status_OK,"Secret does not exist"), nil + } + + //delete secret + err = s.kubeAPI.CoreV1().Secrets(depl.Namespace).Delete(secretName, &metav1.DeleteOptions{}) + if err != nil { + return prepareResponse(v1.Status_FAILED, "Error while removing secret!"), err + } + + return prepareResponse(v1.Status_OK, "Secret deleted successfully"), nil +} + +func (s *certManagerServiceServer) DeleteIfExists(ctx context.Context, req *v1.InstanceRequest) (*v1.ServiceResponse, error) { + // check if the API version requested by client is supported by server + if err := checkAPI(req.Api, apiVersion); err != nil { + return nil, err + } + + depl := req.Deployment + + //check if given k8s namespace exists + _, err := s.kubeAPI.CoreV1().Namespaces().Get(depl.Namespace, metav1.GetOptions{}) + if err != nil { + return prepareResponse(v1.Status_FAILED, namespaceNotFound), err + } + + secretName := depl.Uid + "-tls" + + //check if secret exist + _, err = s.kubeAPI.CoreV1().Secrets(depl.Namespace).Get(secretName, metav1.GetOptions{}) + if err != nil { + return prepareResponse(v1.Status_OK,"Secret does not exist"), nil + } + + //delete secret + err = s.kubeAPI.CoreV1().Secrets(depl.Namespace).Delete(secretName, &metav1.DeleteOptions{}) + if err != nil { + return prepareResponse(v1.Status_FAILED, "Error while removing secret!"), err + } + + return prepareResponse(v1.Status_OK, "Secret deleted successfully"), nil +} + +func (s *readinessServiceServer) CheckIfReady(ctx context.Context, req *v1.InstanceRequest) (*v1.ServiceResponse, error) { + // check if the API version requested by client is supported by server + if err := checkAPI(req.Api, apiVersion); err != nil { + return nil, err + } + + depl := req.Deployment + + //check if given k8s namespace exists + _, err := s.kubeAPI.CoreV1().Namespaces().Get(depl.Namespace, metav1.GetOptions{}) + if err != nil { + return prepareResponse(v1.Status_FAILED, namespaceNotFound), err + } + + app, err := s.kubeAPI.ExtensionsV1beta1().Deployments(depl.Namespace).Get(depl.Uid, metav1.GetOptions{}) + if err != nil { + return prepareResponse(v1.Status_FAILED, "Deployment not found!"), err + } + + if *app.Spec.Replicas == app.Status.ReadyReplicas { + return prepareResponse(v1.Status_OK, "Deployment is ready"), nil + } + + return prepareResponse(v1.Status_PENDING, "Waiting for deployment"), err +} + +func (s *informationServiceServer) RetrieveServiceIp(ctx context.Context, req *v1.InstanceRequest) (*v1.InfoServiceResponse, error) { + + log.Printf("Entered RetrieveServiceIp method") + + // check if the API version requested by client is supported by server + if err := checkAPI(req.Api, apiVersion); err != nil { + return nil, err + } + + depl := req.Deployment + + //check if given k8s namespace exists + _, err := s.kubeAPI.CoreV1().Namespaces().Get(depl.Namespace, metav1.GetOptions{}) + if err != nil { + return prepareInfoResponse(v1.Status_FAILED, namespaceNotFound, ""), err + } + + log.Printf("About to read service %s details from namespace %s", depl.Uid, depl.Namespace) + + app, err := s.kubeAPI.CoreV1().Services(depl.Namespace).Get(depl.Uid, metav1.GetOptions{}) + if err != nil { + return prepareInfoResponse(v1.Status_FAILED, "Service not found!", ""), err + } + + if len(app.Status.LoadBalancer.Ingress) > 0 { + log.Printf("Found %d loadbalancer ingresse(s)", len(app.Status.LoadBalancer.Ingress)) + + ip := app.Status.LoadBalancer.Ingress[0].IP + + if ip != "" { + log.Printf("Found IP address. Will return %s", ip) + return prepareInfoResponse(v1.Status_OK, "", ip), err + } else { + log.Printf("IP adress not found") + return prepareInfoResponse(v1.Status_FAILED, "Ip not found!", ""), err + } + + } else { + log.Printf("No loadbalancer ingresses found") + //return prepareInfoResponse(v1.Status_FAILED, "Service ingress not found!", ""), err + // fake for tests + return prepareInfoResponse(v1.Status_OK, "", "192.168.1.1"), err + } + +} \ No newline at end of file diff --git a/test_version/config-service_test.go b/test_version/config-service_test.go new file mode 100644 index 0000000000000000000000000000000000000000..05060d64a49bb81847252692588a716f14c6a854 --- /dev/null +++ b/test_version/config-service_test.go @@ -0,0 +1,303 @@ +package v1 + +import ( + "code.geant.net/stash/scm/nmaas/nmaas-janitor/pkg/api/v1" + "context" + "github.com/xanzy/go-gitlab" + extension "k8s.io/api/extensions/v1beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "testing" + testclient "k8s.io/client-go/kubernetes/fake" +) + +func TestCheckAPI(t *testing.T) { + api := "wrong" + current := "correct" + err := checkAPI(api, current) + if err == nil { + t.Fail() + } + + api = "correct" + err = checkAPI(api, current) + if err != nil { + t.Fail() + } +} + +var inst = v1.Instance{Namespace: "test-namespace", Uid: "test-uid", Domain: "test-domain"} +var fake_ns_inst = v1.Instance{Namespace: "fake-namespace", Uid: "test-uid", Domain: "test-domain"} + +var req = v1.InstanceRequest{Api: apiVersion, Deployment: &inst} +var illegal_req = v1.InstanceRequest{Api: "illegal", Deployment: &inst} + +func TestReadinessServiceServer_CheckIfReady(t *testing.T) { + client := testclient.NewSimpleClientset() + server := NewReadinessServiceServer(client) + + //Fail on API version check + res, err := server.CheckIfReady(context.Background(), &illegal_req) + if err == nil || res != nil { + t.Fail() + } + + //Fail on namespace check + freq := v1.InstanceRequest{Api:apiVersion, Deployment:&fake_ns_inst} + res, err = server.CheckIfReady(context.Background(), &freq) + if err == nil || res.Status != v1.Status_FAILED { + t.Fail() + } + + //create mock namespace + ns := corev1.Namespace{} + ns.Name = "test-namespace" + _, _ = client.CoreV1().Namespaces().Create(&ns) + + //Fail on deployment + res, err = server.CheckIfReady(context.Background(), &req) + if err == nil || res.Status != v1.Status_FAILED { + t.Fail() + } + + //create mock deployment that is fully deployed + depl := extension.Deployment{} + depl.Name = "test-uid" + q := int32(5) + depl.Spec.Replicas = &q + depl.Status.ReadyReplicas = q + _, _ = client.ExtensionsV1beta1().Deployments("test-namespace").Create(&depl) + + res, err = server.CheckIfReady(context.Background(), &req) + if err != nil || res.Status != v1.Status_OK { + t.Fail() + } + + //modify mock deployment to be partially deployed + p := int32(3) + depl.Status.ReadyReplicas = p + _, _ = client.ExtensionsV1beta1().Deployments("test-namespace").Update(&depl) + + res, err = server.CheckIfReady(context.Background(), &req) + if err != nil || res.Status != v1.Status_PENDING { + t.Fail() + } +} + +func TestInformationServiceServer_RetrieveServiceIp(t *testing.T) { + client := testclient.NewSimpleClientset() + server := NewInformationServiceServer(client) + + //Fail on API version check + res, err := server.RetrieveServiceIp(context.Background(), &illegal_req) + if err == nil || res != nil { + t.Fail() + } + + //Fail on namespace check + freq := v1.InstanceRequest{Api:apiVersion, Deployment:&fake_ns_inst} + res, err = server.RetrieveServiceIp(context.Background(), &freq) + if err == nil || res.Status != v1.Status_FAILED { + t.Fail() + } + + //create mock namespace + ns := corev1.Namespace{} + ns.Name = "test-namespace" + _, _ = client.CoreV1().Namespaces().Create(&ns) + + //Fail on loading services + res, err = server.RetrieveServiceIp(context.Background(), &req) + if err == nil || res.Status != v1.Status_FAILED || res.Message != "Service not found!" { + t.Fail() + } + + //create mock service without ingress + s1 := corev1.Service{} + s1.Name = "test-uid" + _, _ = client.CoreV1().Services("test-namespace").Create(&s1) + + //Fail on missing service ingress + // fake for tests + //res, err = server.RetrieveServiceIp(context.Background(), &req) + //if err != nil || res.Status != v1.Status_FAILED || res.Message != "Service ingress not found!"{ + // t.Fail() + //} + + //create mock service with ingress but no IP + s2 := corev1.Service{} + s2.Name = "test-uid" + i1 := corev1.LoadBalancerIngress{} + ing := []corev1.LoadBalancerIngress{i1} + s2.Status.LoadBalancer.Ingress = ing + client.CoreV1().Services("test-namespace").Delete("test-uid", &metav1.DeleteOptions{}) + _, _ = client.CoreV1().Services("test-namespace").Create(&s2) + + //Fail on missing service ingress IP + // fake for tests + //res, err = server.RetrieveServiceIp(context.Background(), &req) + //if err != nil || res.Status != v1.Status_FAILED || res.Message != "Ip not found!"{ + // t.Fail() + //} + + //create mock service with ingress and IP + s3 := corev1.Service{} + s3.Name = "test-uid" + i2 := corev1.LoadBalancerIngress{} + i2.IP = "10.10.1.1" + ing2 := []corev1.LoadBalancerIngress{i2} + s3.Status.LoadBalancer.Ingress = ing2 + client.CoreV1().Services("test-namespace").Delete("test-uid", &metav1.DeleteOptions{}) + _, _ = client.CoreV1().Services("test-namespace").Create(&s3) + + //Pass + res, err = server.RetrieveServiceIp(context.Background(), &req) + if res.Status != v1.Status_OK || res.Info != "10.10.1.1" { + t.Fail() + } +} + +func TestCertManagerServiceServer_DeleteIfExists(t *testing.T) { + client := testclient.NewSimpleClientset() + server := NewCertManagerServiceServer(client) + + //Fail on API version check + res, err := server.DeleteIfExists(context.Background(), &illegal_req) + if err == nil || res != nil { + t.Fail() + } + + //Fail on namespace check + freq := v1.InstanceRequest{Api:apiVersion, Deployment:&fake_ns_inst} + res, err = server.DeleteIfExists(context.Background(), &freq) + if err == nil || res.Status != v1.Status_FAILED { + t.Fail() + } + + //create mock namespace + ns := corev1.Namespace{} + ns.Name = "test-namespace" + _, _ = client.CoreV1().Namespaces().Create(&ns) + + //Pass if already nonexistent + res, err = server.DeleteIfExists(context.Background(), &req) + if err != nil || res.Status != v1.Status_OK { + t.Fail() + } + + //Create mock secret + sec := corev1.Secret{} + sec.Name = "test-uid-tls" + _, _ = client.CoreV1().Secrets("test-namespace").Create(&sec) + + //Pass + res, err = server.DeleteIfExists(context.Background(), &req) +} + +func TestBasicAuthServiceServer_DeleteIfExists(t *testing.T) { + client := testclient.NewSimpleClientset() + server := NewBasicAuthServiceServer(client) + + res, err := server.DeleteIfExists(context.Background(), &illegal_req) + if err == nil || res != nil { + t.Fail() + } + + //Fail on namespace check + freq := v1.InstanceRequest{Api:apiVersion, Deployment:&fake_ns_inst} + res, err = server.DeleteIfExists(context.Background(), &freq) + if err == nil || res.Status != v1.Status_FAILED { + t.Fail() + } + + //create mock namespace + ns := corev1.Namespace{} + ns.Name = "test-namespace" + _, _ = client.CoreV1().Namespaces().Create(&ns) + + //Pass if already nonexistent + res, err = server.DeleteIfExists(context.Background(), &req) + if err != nil || res.Status != v1.Status_OK { + t.Fail() + } + + //Create mock secret + sec := corev1.Secret{} + sec.Name = getAuthSecretName("test-uid") + _, _ = client.CoreV1().Secrets("test-namespace").Create(&sec) + + //Pass + res, err = server.DeleteIfExists(context.Background(), &req) +} + +func TestBasicAuthServiceServer_CreateOrReplace(t *testing.T) { + client := testclient.NewSimpleClientset() + server := NewBasicAuthServiceServer(client) + + creds := v1.Credentials{User: "test-user", Password: "test-password"} + + //Fail on api test + illreq := v1.InstanceCredentialsRequest{Api: "dummy", Instance: &fake_ns_inst, Credentials: &creds} + res, err := server.CreateOrReplace(context.Background(), &illreq) + if err == nil || res != nil { + t.Fail() + } + + //Should create new secret + req := v1.InstanceCredentialsRequest{Api:apiVersion, Instance:&inst, Credentials: &creds} + res, err = server.CreateOrReplace(context.Background(), &req) + if res.Status != v1.Status_OK || err != nil { + t.Fail() + } + + sec, err := client.CoreV1().Secrets("test-namespace").Get(getAuthSecretName("test-uid"), metav1.GetOptions{}) + if err != nil || sec == nil { + t.Fail() + } + + //Should update secret when already exists + res, err = server.CreateOrReplace(context.Background(), &req) + if res.Status != v1.Status_OK || err != nil { + t.Fail() + } +} + +func TestConfigServiceServer_DeleteIfExists(t *testing.T) { + client := testclient.NewSimpleClientset() + gitclient := gitlab.Client{} + server := NewConfigServiceServer(client, &gitclient) + + //Should fail on api check + res, err := server.DeleteIfExists(context.Background(), &illegal_req) + if err == nil || res != nil { + t.Fail() + } + + //Should fail on namespace check + res, err = server.DeleteIfExists(context.Background(), &req) + if err == nil || res.Status != v1.Status_FAILED { + t.Fail() + } + + //create mock namespace + ns := corev1.Namespace{} + ns.Name = "test-namespace" + _, _ = client.CoreV1().Namespaces().Create(&ns) + + //Should return ok on configmap check if missing + res, err = server.DeleteIfExists(context.Background(), &req) + if err != nil || res.Status != v1.Status_OK { + t.Fail() + } + + //create mock configmap + cm := corev1.ConfigMap{} + cm.Name = "test-uid" + _, _ = client.CoreV1().ConfigMaps("test-namespace").Create(&cm) + + //should pass on deleting existing configmap + res, err = server.DeleteIfExists(context.Background(), &req) + if err != nil || res.Status != v1.Status_OK { + t.Fail() + } +}