diff --git a/api/v1alpha1/project_types.go b/api/v1alpha1/project_types.go index cea2d88..0d20feb 100644 --- a/api/v1alpha1/project_types.go +++ b/api/v1alpha1/project_types.go @@ -84,8 +84,8 @@ type ProjectSpec struct { // ProjectStatus defines the observed state of Project type ProjectStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file + // Jira service desk project ID + ID string `json:"id"` } // +kubebuilder:object:root=true diff --git a/config/crd/bases/jiraservicedesk.stakater.com_projects.yaml b/config/crd/bases/jiraservicedesk.stakater.com_projects.yaml index 2fffef6..f12cb5a 100644 --- a/config/crd/bases/jiraservicedesk.stakater.com_projects.yaml +++ b/config/crd/bases/jiraservicedesk.stakater.com_projects.yaml @@ -90,6 +90,12 @@ spec: type: object status: description: ProjectStatus defines the observed state of Project + properties: + id: + description: Jira service desk project ID + type: string + required: + - id type: object type: object version: v1alpha1 diff --git a/controllers/project_controller.go b/controllers/project_controller.go index 6d8eea4..50433eb 100644 --- a/controllers/project_controller.go +++ b/controllers/project_controller.go @@ -67,23 +67,25 @@ func (r *ProjectReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { } // Check if the Project already exists - // project, err := r.JiraServiceDeskClient.GetProjectByKey(instance.Spec.Key) - // if err != nil { - // return ctrl.Result{}, err - // } - // // Project already exists - // // TODO: This should be project != nil - // if err != nil { - // updatedProject := r.JiraServiceDeskClient.GetProjectFromProjectSpec(instance.Spec) - // if !r.JiraServiceDeskClient.ProjectEqual(project, updatedProject) { - // return r.handleUpdate(req, instance) - // } else { - // log.Info("Skipping update. No changes found") - // return ctrl.Result{}, nil - // } - // } - // TODO: Think of use cases and add a default return ctrl.Result{}, nil - return r.handleCreate(req, instance, log) + if len(instance.Status.ID) > 0 { + project, err := r.JiraServiceDeskClient.GetProjectById(instance.Status.ID) + if err != nil { + return ctrl.Result{}, err + } + // Project already exists + if len(project.Id) > 0 { + updatedProject := r.JiraServiceDeskClient.GetProjectFromProjectSpec(instance.Spec) + // Compare retrieved project with current spec + if !r.JiraServiceDeskClient.ProjectEqual(project, updatedProject) { + // Update if there are changes in the declared spec + return r.handleUpdate(req, instance) + } else { + log.Info("Skipping update. No changes found") + return ctrl.Result{}, nil + } + } + } + return r.handleCreate(req, instance) } func (r *ProjectReconciler) SetupWithManager(mgr ctrl.Manager) error { @@ -92,13 +94,22 @@ func (r *ProjectReconciler) SetupWithManager(mgr ctrl.Manager) error { Complete(r) } -func (r *ProjectReconciler) handleCreate(req ctrl.Request, instance *jiraservicedeskv1alpha1.Project, log logr.Logger) (ctrl.Result, error) { +func (r *ProjectReconciler) handleCreate(req ctrl.Request, instance *jiraservicedeskv1alpha1.Project) (ctrl.Result, error) { + log := r.Log.WithValues("project", req.NamespacedName) log.Info("Creating Jira Service Desk Project: " + instance.Spec.Name) project := r.JiraServiceDeskClient.GetProjectFromProjectSpec(instance.Spec) - err := r.JiraServiceDeskClient.CreateProject(project) + projectId, err := r.JiraServiceDeskClient.CreateProject(project) + if err != nil { + return ctrl.Result{}, err + } + + instance.Status.ID = projectId + + err = r.Status().Update(context.Background(), instance) if err != nil { + log.Error(err, "Failed to update status of Project") return ctrl.Result{}, err } @@ -108,9 +119,22 @@ func (r *ProjectReconciler) handleCreate(req ctrl.Request, instance *jiraservice } func (r *ProjectReconciler) handleDelete(req ctrl.Request, instance *jiraservicedeskv1alpha1.Project) (ctrl.Result, error) { + log := r.Log.WithValues("project", req.NamespacedName) + + log.Info("Deleting Jira Service Desk Project: " + instance.Spec.Name) + + if instance == nil { + // Instance not found, nothing to do + return ctrl.Result{}, nil + } + return ctrl.Result{}, nil } func (r *ProjectReconciler) handleUpdate(req ctrl.Request, instance *jiraservicedeskv1alpha1.Project) (ctrl.Result, error) { + log := r.Log.WithValues("project", req.NamespacedName) + + log.Info("Updating Jira Service Desk Project: " + instance.Spec.Name) + return ctrl.Result{RequeueAfter: defaultRequeueTime}, nil } diff --git a/examples/project/project.yaml b/examples/project/classic-project.yaml similarity index 100% rename from examples/project/project.yaml rename to examples/project/classic-project.yaml diff --git a/examples/project/next-gen-project.yaml b/examples/project/next-gen-project.yaml new file mode 100644 index 0000000..4f6a079 --- /dev/null +++ b/examples/project/next-gen-project.yaml @@ -0,0 +1,14 @@ +apiVersion: jiraservicedesk.stakater.com/v1alpha1 +kind: Project +metadata: + name: stakater +spec: + name: stakater + key: STK + projectTypeKey: service_desk + projectTemplateKey: com.atlassian.servicedesk:next-gen-it-service-desk + description: "Sample project for jira-service-desk-operator" + assigneeType: PROJECT_LEAD + leadAccountId: 5ebfbc3ead226b0ba46c3590 + url: https://stakater.com + diff --git a/jiraservicedesk/client/client.go b/jiraservicedesk/client/client.go index 6829332..640c636 100644 --- a/jiraservicedesk/client/client.go +++ b/jiraservicedesk/client/client.go @@ -14,15 +14,11 @@ import ( var Log = logf.Log.WithName("jiraServiceDeskClient") -const ( - EndpointApiVersion3Project = "/rest/api/3/project" -) - type Client interface { // Methods for Project - GetProjectByKey(key string) (Project, error) + GetProjectById(id string) (Project, error) GetProjectFromProjectSpec(spec jiraservicedeskv1alpha1.ProjectSpec) Project - CreateProject(project Project) error + CreateProject(project Project) (string, error) UpdateProject(updatedProject Project) (Project, error) ProjectEqual(oldProject Project, newProject Project) bool } diff --git a/jiraservicedesk/client/client_project.go b/jiraservicedesk/client/client_project.go index 54a7a0b..6207f69 100644 --- a/jiraservicedesk/client/client_project.go +++ b/jiraservicedesk/client/client_project.go @@ -1,6 +1,7 @@ package client import ( + "encoding/json" "errors" "io/ioutil" "strconv" @@ -8,6 +9,15 @@ import ( jiraservicedeskv1alpha1 "github.com/stakater/jira-service-desk-operator/api/v1alpha1" ) +const ( + // Endpoints + EndpointApiVersion3Project = "/rest/api/3/project" + + // Project Template Types + ClassicProjectTemplateKey = "com.atlassian.servicedesk:itil-v2-service-desk-project" + NextGenProjectTemplateKey = "com.atlassian.servicedesk:next-gen-it-service-desk" +) + type Project struct { Id string `json:"id,omitempty"` Name string `json:"name,omitempty"` @@ -25,65 +35,102 @@ type Project struct { CategoryId int `json:"categoryId,omitempty"` } -func NewProject(name string) Project { - return Project{ - Name: name, - } +type ProjectGetResponse struct { + Self string `json:"self,omitempty"` + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Key string `json:"key,omitempty"` + Description string `json:"description,omitempty"` + Lead ProjectLead `json:"lead,omitempty"` + ProjectTypeKey string `json:"projectTypeKey,omitempty"` + Style string `json:"style,omitempty"` + AssigneeType string `json:"assigneeType,omitempty"` + URL string `json:"url,omitempty"` } -func (c *jiraServiceDeskClient) GetProjectByKey(name string) (Project, error) { - return NewProject("test"), nil +type ProjectLead struct { + Self string `json:"self,omitempty"` + AccountId string `json:"accountId,omitempty"` +} + +type ProjectCreateResponse struct { + Self string `json:"self"` + Id int `json:"id"` + Key string `json:"key"` +} + +func (c *jiraServiceDeskClient) GetProjectById(id string) (Project, error) { + var project Project + + request, err := c.newRequest("GET", EndpointApiVersion3Project+"/"+id, nil) + if err != nil { + return project, err + } + + response, err := c.do(request) + if err != nil { + return project, err + } + defer response.Body.Close() + + var responseObject ProjectGetResponse + err = json.NewDecoder(response.Body).Decode(&responseObject) + if err != nil { + return project, err + } + + project = projectGetResponseToProjectMapper(responseObject) + return project, err } -func (c *jiraServiceDeskClient) CreateProject(project Project) error { +func (c *jiraServiceDeskClient) CreateProject(project Project) (string, error) { request, err := c.newRequest("POST", EndpointApiVersion3Project, project) if err != nil { - return err + return "", err } response, err := c.do(request) if err != nil { - return err + return "", err } defer response.Body.Close() + responseData, _ := ioutil.ReadAll(response.Body) if response.StatusCode < 200 || response.StatusCode > 299 { - data, _ := ioutil.ReadAll(response.Body) err := errors.New("Rest request to create Project failed with status " + strconv.Itoa(response.StatusCode) + - " and response: " + string(data)) - return err + " and response: " + string(responseData)) + return "", err } - return err + var responseObject ProjectCreateResponse + err = json.Unmarshal(responseData, &responseObject) + if err != nil { + return "", err + } + projectId := strconv.Itoa(responseObject.Id) + + return projectId, err } func (c *jiraServiceDeskClient) UpdateProject(updatedProject Project) (Project, error) { - return NewProject("test"), nil + return Project{Name: updatedProject.Name}, nil } func (c *jiraServiceDeskClient) ProjectEqual(oldProject Project, newProject Project) bool { - return false + // The fields AvatarId, IssueSecurityScheme, NotificationScheme, PermissionScheme, CategoryId are not retrieved + // through get project REST API call so they cannot be used in project comparison + return oldProject.Id == newProject.Id && + oldProject.Name == newProject.Name && + oldProject.Key == newProject.Key && + oldProject.ProjectTypeKey == newProject.ProjectTypeKey && + oldProject.ProjectTemplateKey == newProject.ProjectTemplateKey && + oldProject.Description == newProject.Description && + oldProject.AssigneeType == newProject.AssigneeType && + oldProject.LeadAccountId == newProject.LeadAccountId && + oldProject.URL == newProject.URL } func (c *jiraServiceDeskClient) GetProjectFromProjectSpec(spec jiraservicedeskv1alpha1.ProjectSpec) Project { return projectSpecToProjectMapper(spec) } - -func projectSpecToProjectMapper(spec jiraservicedeskv1alpha1.ProjectSpec) Project { - return Project{ - Name: spec.Name, - Key: spec.Key, - ProjectTypeKey: spec.ProjectTypeKey, - ProjectTemplateKey: spec.ProjectTemplateKey, - Description: spec.Description, - AssigneeType: spec.AssigneeType, - LeadAccountId: spec.LeadAccountId, - URL: spec.URL, - AvatarId: spec.AvatarId, - IssueSecurityScheme: spec.IssueSecurityScheme, - PermissionScheme: spec.IssueSecurityScheme, - NotificationScheme: spec.NotificationScheme, - CategoryId: spec.CategoryId, - } -} diff --git a/jiraservicedesk/client/client_project_mappers.go b/jiraservicedesk/client/client_project_mappers.go new file mode 100644 index 0000000..f65e0ce --- /dev/null +++ b/jiraservicedesk/client/client_project_mappers.go @@ -0,0 +1,46 @@ +package client + +import ( + jiraservicedeskv1alpha1 "github.com/stakater/jira-service-desk-operator/api/v1alpha1" +) + +func projectSpecToProjectMapper(spec jiraservicedeskv1alpha1.ProjectSpec) Project { + return Project{ + Name: spec.Name, + Key: spec.Key, + ProjectTypeKey: spec.ProjectTypeKey, + ProjectTemplateKey: spec.ProjectTemplateKey, + Description: spec.Description, + AssigneeType: spec.AssigneeType, + LeadAccountId: spec.LeadAccountId, + URL: spec.URL, + AvatarId: spec.AvatarId, + IssueSecurityScheme: spec.IssueSecurityScheme, + PermissionScheme: spec.IssueSecurityScheme, + NotificationScheme: spec.NotificationScheme, + CategoryId: spec.CategoryId, + } +} + +func projectGetResponseToProjectMapper(response ProjectGetResponse) Project { + var projectTemplateKey string + if len(response.Style) > 0 { + if response.Style == "classic" { + projectTemplateKey = ClassicProjectTemplateKey + } else if response.Style == "next-gen" { + projectTemplateKey = NextGenProjectTemplateKey + } + } + + return Project{ + Id: response.Id, + Name: response.Name, + Key: response.Key, + ProjectTypeKey: response.ProjectTypeKey, + ProjectTemplateKey: projectTemplateKey, + Description: response.Description, + AssigneeType: response.AssigneeType, + LeadAccountId: response.Lead.AccountId, + URL: response.URL, + } +}