From 3a0a9e189e98fa10004b8df18f23d2dc06b1f54b Mon Sep 17 00:00:00 2001 From: Andrew Sheng <44681431+andrewsheng2@users.noreply.github.com> Date: Wed, 20 Sep 2023 13:50:45 -0400 Subject: [PATCH] Add support to manage project features (#17) Changing the artifact storage for a project is done through project features, add support to the TeamCity client used by our Terraform provider to allow managing project features This was based on existing code to manage build features TEST=manual Testing was done in conjunction with changes done with the Terraform provider, applied a pipeline to a local TeamCity where a TeamCity project was made with some random project feature --- teamcity/build_feature.go | 23 ++-- teamcity/generic_project_feature.go | 94 +++++++++++++ teamcity/project_feature.go | 197 ++++++++++++++++++++++++++++ teamcity/teamcity.go | 21 +-- 4 files changed, 315 insertions(+), 20 deletions(-) create mode 100644 teamcity/generic_project_feature.go create mode 100644 teamcity/project_feature.go diff --git a/teamcity/build_feature.go b/teamcity/build_feature.go index ac24b8d..48e8b3e 100644 --- a/teamcity/build_feature.go +++ b/teamcity/build_feature.go @@ -10,7 +10,7 @@ import ( "github.com/dghubble/sling" ) -//BuildFeature is an interface representing different types of build features that can be added to a build type. +// BuildFeature is an interface representing different types of build features that can be added to a build type. type BuildFeature interface { ID() string SetID(value string) @@ -43,7 +43,7 @@ type Features struct { Items []buildFeatureJSON `json:"feature"` } -//BuildFeatureService provides operations for managing build features for a buildType +// BuildFeatureService provides operations for managing build features for a buildType type BuildFeatureService struct { BuildTypeID string httpClient *http.Client @@ -61,7 +61,7 @@ func newBuildFeatureService(buildTypeID string, c *http.Client, base *sling.Slin } } -//Create adds a new build feature to build type +// Create adds a new build feature to build type func (s *BuildFeatureService) Create(bf BuildFeature) (BuildFeature, error) { if bf == nil { return nil, errors.New("bf can't be nil") @@ -87,7 +87,7 @@ func (s *BuildFeatureService) Create(bf BuildFeature) (BuildFeature, error) { return s.readBuildFeatureResponse(resp) } -//GetByID returns a build feature by its id +// GetByID returns a build feature by its id func (s *BuildFeatureService) GetByID(id string) (BuildFeature, error) { req, err := s.base.New().Get(id).Request() @@ -126,18 +126,18 @@ func (s *BuildFeatureService) GetBuildFeatures() ([]BuildFeature, error) { return nil, err } - cbf := GenericBuildFeature{} - err = cbf.UnmarshalJSON(dt) + gbf := GenericBuildFeature{} + err = gbf.UnmarshalJSON(dt) if err != nil { return nil, err } - buildFeatures[i] = &cbf + buildFeatures[i] = &gbf } return buildFeatures, nil } -//Delete removes a build feature from the build configuration by its id. +// Delete removes a build feature from the build configuration by its id. func (s *BuildFeatureService) Delete(id string) error { request, _ := s.base.New().Delete(id).Request() response, err := s.httpClient.Do(request) @@ -207,14 +207,13 @@ func (s *BuildFeatureService) readBuildFeatureResponse(resp *http.Response) (Bui if err := csp.UnmarshalJSON(bodyBytes); err != nil { return nil, err } - out = &csp default: - var cbf GenericBuildFeature - if err := cbf.UnmarshalJSON(bodyBytes); err != nil { + var gbf GenericBuildFeature + if err := gbf.UnmarshalJSON(bodyBytes); err != nil { return nil, err } - return out, nil + out = &gbf } out.SetBuildTypeID(s.BuildTypeID) diff --git a/teamcity/generic_project_feature.go b/teamcity/generic_project_feature.go new file mode 100644 index 0000000..24a75a4 --- /dev/null +++ b/teamcity/generic_project_feature.go @@ -0,0 +1,94 @@ +package teamcity + +import ( + "encoding/json" +) + +type GenericProjectFeature struct { + id string + featureType string + projectID string + disabled bool + properties *Properties +} + +func (pf *GenericProjectFeature) ID() string { + return pf.id +} + +func (pf *GenericProjectFeature) SetID(value string) { + pf.id = value +} + +func (pf *GenericProjectFeature) Type() string { + return pf.featureType +} + +func (pf *GenericProjectFeature) Properties() *Properties { + return pf.properties +} + +func (pf *GenericProjectFeature) ProjectID() string { + return pf.projectID +} + +func (pf *GenericProjectFeature) SetProjectID(value string) { + pf.projectID = value +} + +func (pf *GenericProjectFeature) Disabled() bool { + return pf.disabled +} + +func (pf *GenericProjectFeature) SetDisabled(value bool) { + pf.disabled = value +} + +func (pf *GenericProjectFeature) MarshalJSON() ([]byte, error) { + out := &projectFeatureJSON{ + ID: pf.id, + Disabled: NewBool(pf.disabled), + Properties: pf.properties, + Inherited: NewFalse(), + Type: pf.Type(), + } + + return json.Marshal(out) +} + +func (pf *GenericProjectFeature) UnmarshalJSON(data []byte) error { + var aux projectFeatureJSON + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + pf.id = aux.ID + pf.featureType = aux.Type + + disabled := aux.Disabled + if disabled == nil { + disabled = NewFalse() + } + pf.disabled = *disabled + + if aux.Properties != nil { + pf.properties = NewProperties(aux.Properties.Items...) + } + + return nil +} + +func NewGenericProjectFeature(featureType string, propertiesRaw map[string]interface{}) (*GenericProjectFeature, error) { + properties := NewPropertiesEmpty() + for name, value := range propertiesRaw { + value := value.(string) + properties.Add(&Property{ + Name: name, + Value: value, + }) + } + + return &GenericProjectFeature{ + featureType: featureType, + properties: properties, + }, nil +} diff --git a/teamcity/project_feature.go b/teamcity/project_feature.go new file mode 100644 index 0000000..48a87ca --- /dev/null +++ b/teamcity/project_feature.go @@ -0,0 +1,197 @@ +package teamcity + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + + "github.com/dghubble/sling" +) + +// ProjectFeature is an interface representing different types of project features that can be added to a project. +type ProjectFeature interface { + ID() string + SetID(value string) + Type() string + Properties() *Properties + ProjectID() string + SetProjectID(value string) + Disabled() bool + SetDisabled(value bool) + MarshalJSON() ([]byte, error) + UnmarshalJSON(data []byte) error +} + +type projectFeatureJSON struct { + Disabled *bool `json:"disabled,omitempty" xml:"disabled"` + Href string `json:"href,omitempty" xml:"href"` + ID string `json:"id,omitempty" xml:"id"` + Inherited *bool `json:"inherited,omitempty" xml:"inherited"` + Name string `json:"name,omitempty" xml:"name"` + Properties *Properties `json:"properties,omitempty"` + Type string `json:"type,omitempty" xml:"type"` +} + +// ProjectFeatures is a collection of ProjectFeature +type ProjectFeatures struct { + Count int32 `json:"count,omitempty" xml:"count"` + Href string `json:"href,omitempty" xml:"href"` + Items []projectFeatureJSON `json:"projectFeature"` +} + +// ProjectFeatureService provides operations for managing project features for a project +type ProjectFeatureService struct { + ProjectID string + httpClient *http.Client + base *sling.Sling + restHelper *restHelper +} + +func newProjectFeatureService(projectID string, c *http.Client, base *sling.Sling) *ProjectFeatureService { + slingName := base.New().Path(fmt.Sprintf("projects/%s/projectFeatures/", projectID)) + return &ProjectFeatureService{ + ProjectID: projectID, + httpClient: c, + base: slingName, + restHelper: newRestHelperWithSling(c, slingName), + } +} + +// Create adds a new project feature to project +func (s *ProjectFeatureService) Create(pf ProjectFeature) (ProjectFeature, error) { + if pf == nil { + return nil, errors.New("pf can't be nil") + } + + req, err := s.base.New().Post("").BodyJSON(pf).Request() + + if err != nil { + return nil, err + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return nil, fmt.Errorf("Unknown error when adding project feature, statusCode: %d", resp.StatusCode) + } + + return s.readProjectFeatureResponse(resp) +} + +// GetByID returns a project feature by its id +func (s *ProjectFeatureService) GetByID(id string) (ProjectFeature, error) { + req, err := s.base.New().Get(id).Request() + + if err != nil { + return nil, err + } + + resp, err := s.httpClient.Do(req) + + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + if resp.StatusCode == 404 { + return nil, fmt.Errorf("404 Not Found - Project feature (id: %s) for projectId (id: %s) was not found", id, s.ProjectID) + } + + return s.readProjectFeatureResponse(resp) +} + +// GetProjectFeatures gets all the project features of a Project +func (s *ProjectFeatureService) GetProjectFeatures() ([]ProjectFeature, error) { + var features ProjectFeatures + err := s.restHelper.get("", &features, "project features") + if err != nil { + return nil, err + } + + projectFeatures := make([]ProjectFeature, features.Count) + + for i := range features.Items { + dt, err := json.Marshal(features.Items[i]) + if err != nil { + return nil, err + } + + gpf := GenericProjectFeature{} + err = gpf.UnmarshalJSON(dt) + if err != nil { + return nil, err + } + projectFeatures[i] = &gpf + } + + return projectFeatures, nil +} + +// Delete removes a project feature from the project configuration by its id. +func (s *ProjectFeatureService) Delete(id string) error { + request, _ := s.base.New().Delete(id).Request() + response, err := s.httpClient.Do(request) + if err != nil { + return err + } + + defer response.Body.Close() + if response.StatusCode == 204 { + return nil + } + + if response.StatusCode != 200 && response.StatusCode != 204 { + respData, err := ioutil.ReadAll(response.Body) + if err != nil { + return err + } + return fmt.Errorf("Error '%d' when deleting project feature: %s", response.StatusCode, string(respData)) + } + + return nil +} + +// DeleteAll removes all project features of a project configuration +func (s *ProjectFeatureService) DeleteAll() error { + features, err := s.GetProjectFeatures() + if err != nil { + return err + } + + for _, feature := range features { + if err := s.Delete(feature.ID()); err != nil { + return err + } + } + + return nil +} + +func (s *ProjectFeatureService) readProjectFeatureResponse(resp *http.Response) (ProjectFeature, error) { + bodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var payload projectFeatureJSON + if err := json.Unmarshal(bodyBytes, &payload); err != nil { + return nil, err + } + + var out ProjectFeature + var gpf GenericProjectFeature + if err := gpf.UnmarshalJSON(bodyBytes); err != nil { + return nil, err + } + out = &gpf + out.SetProjectID(s.ProjectID) + return out, nil +} diff --git a/teamcity/teamcity.go b/teamcity/teamcity.go index 5a05960..6782dda 100644 --- a/teamcity/teamcity.go +++ b/teamcity/teamcity.go @@ -15,10 +15,10 @@ import ( _ "github.com/motemen/go-loghttp/global" ) -//DebugRequests toggle to enable tracing requests to stdout +// DebugRequests toggle to enable tracing requests to stdout var DebugRequests = false -//DebugResponses toggle to enable tracing responses to stdout +// DebugResponses toggle to enable tracing responses to stdout var DebugResponses = false func init() { @@ -34,7 +34,7 @@ func init() { } } -//Client represents the base for connecting to TeamCity +// Client represents the base for connecting to TeamCity type Client struct { userName, password, address string baseURI string @@ -86,27 +86,32 @@ func newClientInstance(userName, password, address string, httpClient *http.Clie }, nil } -//AgentRequirementService returns a service to manage agent requirements for a build configuration with given id +// AgentRequirementService returns a service to manage agent requirements for a build configuration with given id func (c *Client) AgentRequirementService(id string) *AgentRequirementService { return newAgentRequirementService(id, c.HTTPClient, c.commonBase.New()) } -//BuildFeatureService returns a service to manage agent requirements for a build configuration with given id +// BuildFeatureService returns a service to manage build features for a build configuration with given id func (c *Client) BuildFeatureService(id string) *BuildFeatureService { return newBuildFeatureService(id, c.HTTPClient, c.commonBase.New()) } -//DependencyService returns a service to manage snapshot and artifact dependencies for a build configuration with given id +// ProjectFeatureService returns a service to manage project features for a project with given id +func (c *Client) ProjectFeatureService(id string) *ProjectFeatureService { + return newProjectFeatureService(id, c.HTTPClient, c.commonBase.New()) +} + +// DependencyService returns a service to manage snapshot and artifact dependencies for a build configuration with given id func (c *Client) DependencyService(id string) *DependencyService { return NewDependencyService(id, c.HTTPClient, c.commonBase.New()) } -//BuildTemplateService returns a service to manage template associations for a build configuration with given id +// BuildTemplateService returns a service to manage template associations for a build configuration with given id func (c *Client) BuildTemplateService(id string) *BuildTemplateService { return NewBuildTemplateService(id, c.HTTPClient, c.commonBase.New()) } -//TriggerService returns a service to manage build triggers for a build configuration with given id +// TriggerService returns a service to manage build triggers for a build configuration with given id func (c *Client) TriggerService(buildTypeID string) *TriggerService { return newTriggerService(buildTypeID, c.HTTPClient, c.commonBase.New()) }