Skip to content

Commit 473a379

Browse files
authored
Merge pull request #9 from OctopusDeploy/autocreateparent
Automatically create parent projects
2 parents 6889827 + 4f4ebe9 commit 473a379

File tree

1 file changed

+122
-24
lines changed

1 file changed

+122
-24
lines changed

main.go

Lines changed: 122 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"bytes"
55
"context"
6+
"encoding/json"
67
"fmt"
78
"io"
89
"mime/multipart"
@@ -21,6 +22,23 @@ var (
2122
v *viper.Viper
2223
)
2324

25+
type Tag struct {
26+
Name string `json:"name"`
27+
}
28+
29+
type Project struct {
30+
UUID string `json:"uuid,omitempty"`
31+
Name string `json:"name"`
32+
Classifier string `json:"classifier,omitempty"`
33+
Version string `json:"version,omitempty"`
34+
Description string `json:"description,omitempty"`
35+
Active bool `json:"active,omitempty"`
36+
Tags []Tag `json:"tags,omitempty"`
37+
Children []Project `json:"children,omitempty"`
38+
Parent *Project `json:"parent,omitempty"`
39+
CollectionLogic string `json:"collectionLogic,omitempty"`
40+
}
41+
2442
func main() {
2543
rootCmd := &cobra.Command{
2644
Use: "sbom-uploader",
@@ -48,27 +66,76 @@ func main() {
4866
}
4967
}
5068

51-
func runUploader(cmd *cobra.Command, args []string) error {
69+
func createParent(dependencyTrackUrl string, dependencyTrackKey string, parentName string, tags string) error {
70+
url := fmt.Sprintf("%s/api/v1/project", strings.TrimRight(dependencyTrackUrl, "/"))
71+
newProject := &Project{
72+
Name: parentName,
73+
Classifier: "APPLICATION",
74+
CollectionLogic: "AGGREGATE_LATEST_VERSION_CHILDREN",
75+
Tags: []Tag{},
76+
}
5277

53-
dependencyTrackUrl := v.GetString("url")
54-
dependencyTrackKey := v.GetString("api-key")
55-
projectName := v.GetString("name")
56-
projectVersion := v.GetString("version")
57-
sbomFilePath := v.GetString("sbom")
58-
// Check required inputs
59-
if dependencyTrackUrl == "" {
60-
return fmt.Errorf("missing required inputs: url (via flags or env)")
78+
for _, tag := range strings.Split(tags, ",") {
79+
newProject.Tags = append(newProject.Tags, Tag{Name: tag})
6180
}
62-
if dependencyTrackKey == "" {
63-
return fmt.Errorf("missing required inputs: api-key (via flags or env)")
81+
82+
jsonBody, err := json.Marshal(newProject)
83+
if err != nil {
84+
return fmt.Errorf("failed to marshal request body: %w", err)
6485
}
65-
if projectName == "" {
66-
return fmt.Errorf("missing required inputs: name (via flags or env)")
86+
reqBody := bytes.NewBuffer(jsonBody)
87+
88+
req, err := retryablehttp.NewRequest("PUT", url, reqBody)
89+
if err != nil {
90+
return fmt.Errorf("failed to create request: %w", err)
6791
}
68-
if projectVersion == "" {
69-
return fmt.Errorf("missing required inputs: version (via flags or env)")
92+
req.Header.Set("X-Api-Key", dependencyTrackKey)
93+
req.Header.Set("Content-Type", "application/json")
94+
retryClient := retryablehttp.NewClient()
95+
retryClient.RetryMax = 20
96+
resp, err := retryClient.Do(req)
97+
if err != nil {
98+
return fmt.Errorf("HTTP request failed: %w", err)
99+
}
100+
defer func(Body io.ReadCloser) {
101+
_ = Body.Close()
102+
}(resp.Body)
103+
104+
respBody, _ := io.ReadAll(resp.Body)
105+
106+
print(respBody)
107+
return nil
108+
}
109+
110+
func ensureParentExists(dependencyTrackUrl string, dependencyTrackKey string, parentName string, tags string) error {
111+
url := fmt.Sprintf("%s/api/v1/project/lookup?name=%s", strings.TrimRight(dependencyTrackUrl, "/"), parentName)
112+
req, err := retryablehttp.NewRequest("GET", url, nil)
113+
if err != nil {
114+
return fmt.Errorf("failed to create request: %w", err)
115+
}
116+
req.Header.Set("X-Api-Key", dependencyTrackKey)
117+
118+
// Execute request
119+
retryClient := retryablehttp.NewClient()
120+
retryClient.RetryMax = 20
121+
resp, err := retryClient.Do(req)
122+
if err != nil {
123+
return fmt.Errorf("HTTP request failed: %w", err)
70124
}
125+
var project Project
126+
err = json.NewDecoder(resp.Body).Decode(&project)
127+
if project.Name == "" {
128+
fmt.Println("Parent project not found... creating a new one")
129+
err := createParent(dependencyTrackUrl, dependencyTrackKey, parentName, tags)
130+
if err != nil {
131+
return err
132+
}
133+
}
134+
135+
return nil
136+
}
71137

138+
func uploadSbom(dependencyTrackUrl string, dependencyTrackKey string, projectName string, parentName string, projectVersion string, sbomFilePath string, tags string) error {
72139
// Read SBOM from file or stdin
73140
var sbomContent []byte
74141
var err error
@@ -102,18 +169,14 @@ func runUploader(cmd *cobra.Command, args []string) error {
102169

103170
// Add metadata fields
104171
_ = writer.WriteField("projectName", projectName)
172+
_ = writer.WriteField("parentName", parentName)
105173
_ = writer.WriteField("projectVersion", projectVersion)
106174
_ = writer.WriteField("autoCreate", "true")
175+
_ = writer.WriteField("tags", tags)
107176

108177
if v.GetBool("latest") {
109178
_ = writer.WriteField("isLatest", "true")
110179
}
111-
if v := v.GetString("parent"); v != "" {
112-
_ = writer.WriteField("parentName", v)
113-
}
114-
if projectTags := v.GetString("tags"); projectTags != "" {
115-
_ = writer.WriteField("projectTags", projectTags)
116-
}
117180

118181
// Close writer to finalize body
119182
if err := writer.Close(); err != nil {
@@ -141,10 +204,45 @@ func runUploader(cmd *cobra.Command, args []string) error {
141204
_ = Body.Close()
142205
}(resp.Body)
143206

144-
respBody, _ := io.ReadAll(resp.Body)
207+
return nil
208+
}
209+
210+
func runUploader(cmd *cobra.Command, args []string) error {
211+
212+
dependencyTrackUrl := v.GetString("url")
213+
dependencyTrackKey := v.GetString("api-key")
214+
projectName := v.GetString("name")
215+
parentName := v.GetString("parent")
216+
projectVersion := v.GetString("version")
217+
sbomFilePath := v.GetString("sbom")
218+
// Check required inputs
219+
if dependencyTrackUrl == "" {
220+
return fmt.Errorf("missing required inputs: url (via flags or env)")
221+
}
222+
if dependencyTrackKey == "" {
223+
return fmt.Errorf("missing required inputs: api-key (via flags or env)")
224+
}
225+
if projectName == "" {
226+
return fmt.Errorf("missing required inputs: name (via flags or env)")
227+
}
228+
if parentName == "" {
229+
return fmt.Errorf("missing required inputs: name (via flags or env)")
230+
}
231+
if projectVersion == "" {
232+
return fmt.Errorf("missing required inputs: version (via flags or env)")
233+
}
234+
tags := ""
235+
if projectTags := v.GetString("tags"); projectTags != "" {
236+
tags = projectTags
237+
}
145238

146-
if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusOK {
147-
return fmt.Errorf("upload failed (%d): %s", resp.StatusCode, string(respBody))
239+
err := ensureParentExists(dependencyTrackUrl, dependencyTrackKey, parentName, tags)
240+
if err != nil {
241+
return err
242+
}
243+
err = uploadSbom(dependencyTrackUrl, dependencyTrackKey, projectName, parentName, projectVersion, sbomFilePath, tags)
244+
if err != nil {
245+
return err
148246
}
149247

150248
fmt.Println("✅ SBOM upload successful.")

0 commit comments

Comments
 (0)