diff --git a/cmd/osbuild-tests/main_test.go b/cmd/osbuild-tests/main_test.go index 440bef5f8..e53144c4d 100644 --- a/cmd/osbuild-tests/main_test.go +++ b/cmd/osbuild-tests/main_test.go @@ -112,7 +112,8 @@ func TestSourcesCommands(t *testing.T) { require.NoErrorf(t, err, "Could not create temporary file: %v", err) defer os.Remove(sources_toml.Name()) - _, err = sources_toml.Write([]byte(`name = "osbuild-test-addon-source" + _, err = sources_toml.Write([]byte(`id = "osbuild-test-addon-source" +name = "Testing sources add command" url = "file://REPO-PATH" type = "yum-baseurl" proxy = "https://proxy-url/" diff --git a/internal/client/source.go b/internal/client/source.go index d6123eaac..efa93af78 100644 --- a/internal/client/source.go +++ b/internal/client/source.go @@ -63,6 +63,16 @@ func PostJSONSourceV0(socket *http.Client, source string) (*APIResponse, error) return NewAPIResponse(body) } +// PostJSONSourceV1 sends a JSON source string to the API +// and returns an APIResponse +func PostJSONSourceV1(socket *http.Client, source string) (*APIResponse, error) { + body, resp, err := PostJSON(socket, "/api/v1/projects/source/new", source) + if resp != nil || err != nil { + return resp, err + } + return NewAPIResponse(body) +} + // PostTOMLSourceV0 sends a TOML source string to the API // and returns an APIResponse func PostTOMLSourceV0(socket *http.Client, source string) (*APIResponse, error) { @@ -73,6 +83,16 @@ func PostTOMLSourceV0(socket *http.Client, source string) (*APIResponse, error) return NewAPIResponse(body) } +// PostTOMLSourceV1 sends a TOML source string to the API +// and returns an APIResponse +func PostTOMLSourceV1(socket *http.Client, source string) (*APIResponse, error) { + body, resp, err := PostTOML(socket, "/api/v1/projects/source/new", source) + if resp != nil || err != nil { + return resp, err + } + return NewAPIResponse(body) +} + // DeleteSourceV0 deletes the named source and returns an APIResponse func DeleteSourceV0(socket *http.Client, sourceName string) (*APIResponse, error) { body, resp, err := DeleteRaw(socket, "/api/v0/projects/source/delete/"+sourceName) diff --git a/internal/client/source_test.go b/internal/client/source_test.go index 266c40c9d..cedc5dc23 100644 --- a/internal/client/source_test.go +++ b/internal/client/source_test.go @@ -37,14 +37,21 @@ func TestPOSTJSONSourceV0(t *testing.T) { require.True(t, resp.Status, "DELETE source failed: %#v", resp) } -// POST an empty JSON source +// POST an empty JSON source using V0 API func TestPOSTEmptyJSONSourceV0(t *testing.T) { resp, err := PostJSONSourceV0(testState.socket, "") require.NoError(t, err, "POST source failed with a client error") require.False(t, resp.Status, "did not return an error") } -// POST an invalid JSON source +// POST an empty JSON source using V1 API +func TestPOSTEmptyJSONSourceV1(t *testing.T) { + resp, err := PostJSONSourceV1(testState.socket, "") + require.NoError(t, err, "POST source failed with a client error") + require.False(t, resp.Status, "did not return an error") +} + +// POST an invalid JSON source using V0 API func TestPOSTInvalidJSONSourceV0(t *testing.T) { // Missing quote in url source := `{ @@ -62,7 +69,26 @@ func TestPOSTInvalidJSONSourceV0(t *testing.T) { require.False(t, resp.Status, "did not return an error") } -// POST a new TOML source +// POST an invalid JSON source using V1 API +func TestPOSTInvalidJSONSourceV1(t *testing.T) { + // Missing quote in url + source := `{ + "id": "package-repo-json-v1", + "name": "json package repo", + "url": "file://REPO-PATH, + "type": "yum-baseurl", + "proxy": "https://proxy-url/", + "check_ssl": true, + "check_gpg": true, + "gpgkey_urls": ["https://url/path/to/gpg-key"] + }` + + resp, err := PostJSONSourceV1(testState.socket, source) + require.NoError(t, err, "POST source failed with a client error") + require.False(t, resp.Status, "did not return an error") +} + +// POST a new TOML source using V0 API func TestPOSTTOMLSourceV0(t *testing.T) { source := ` name = "package-repo-toml-v0" @@ -84,14 +110,45 @@ func TestPOSTTOMLSourceV0(t *testing.T) { require.True(t, resp.Status, "DELETE source failed: %#v", resp) } -// POST an empty TOML source +// POST a new TOML source using V1 API +func TestPOSTTOMLSourceV1(t *testing.T) { + source := ` + id = "package-repo-toml-v1" + name = "toml package repo" + url = "file://REPO-PATH" + type = "yum-baseurl" + proxy = "https://proxy-url/" + check_ssl = true + check_gpg = true + gpgkey_urls = ["https://url/path/to/gpg-key"] + ` + source = strings.Replace(source, "REPO-PATH", testState.repoDir, 1) + + resp, err := PostTOMLSourceV1(testState.socket, source) + require.NoError(t, err, "POST source failed with a client error") + require.True(t, resp.Status, "POST source failed: %#v", resp) + + // TODO update for DeleteJSONSourceV1 + resp, err = DeleteSourceV0(testState.socket, "package-repo-toml-v0") + require.NoError(t, err, "DELETE source failed with a client error") + require.True(t, resp.Status, "DELETE source failed: %#v", resp) +} + +// POST an empty TOML source using V0 API func TestPOSTEmptyTOMLSourceV0(t *testing.T) { resp, err := PostTOMLSourceV0(testState.socket, "") require.NoError(t, err, "POST source failed with a client error") require.False(t, resp.Status, "did not return an error") } -// POST an invalid TOML source +// POST an empty TOML source using V1 API +func TestPOSTEmptyTOMLSourceV1(t *testing.T) { + resp, err := PostTOMLSourceV1(testState.socket, "") + require.NoError(t, err, "POST source failed with a client error") + require.False(t, resp.Status, "did not return an error") +} + +// POST an invalid TOML source using V0 API func TestPOSTInvalidTOMLSourceV0(t *testing.T) { // Missing quote in url source := ` @@ -109,7 +166,26 @@ func TestPOSTInvalidTOMLSourceV0(t *testing.T) { require.False(t, resp.Status, "did not return an error") } -// POST a wrong TOML source +// POST an invalid TOML source using V1 API +func TestPOSTInvalidTOMLSourceV1(t *testing.T) { + // Missing quote in url + source := ` + id = "package-repo-toml-v1" + name = "toml package repo" + url = "file://REPO-PATH + type = "yum-baseurl" + proxy = "https://proxy-url/" + check_ssl = true + check_gpg = true + gpgkey_urls = ["https://url/path/to/gpg-key"] + ` + + resp, err := PostTOMLSourceV1(testState.socket, source) + require.NoError(t, err, "POST source failed with a client error") + require.False(t, resp.Status, "did not return an error") +} + +// POST a wrong TOML source using V0 API func TestPOSTWrongTOMLSourceV0(t *testing.T) { // Should not have a [] section source := ` @@ -128,6 +204,26 @@ func TestPOSTWrongTOMLSourceV0(t *testing.T) { require.False(t, resp.Status, "did not return an error") } +// POST a wrong TOML source using V1 API +func TestPOSTWrongTOMLSourceV1(t *testing.T) { + // Should not have a [] section + source := ` + [package-repo-toml-v1] + id = "package-repo-toml-v1" + name = "toml package repo" + url = "file://REPO-PATH + type = "yum-baseurl" + proxy = "https://proxy-url/" + check_ssl = true + check_gpg = true + gpgkey_urls = ["https://url/path/to/gpg-key"] + ` + + resp, err := PostTOMLSourceV1(testState.socket, source) + require.NoError(t, err, "POST source failed with a client error") + require.False(t, resp.Status, "did not return an error") +} + // list sources using the v0 API func TestListSourcesV0(t *testing.T) { sources := []string{`{ @@ -177,7 +273,8 @@ func TestListSourcesV0(t *testing.T) { // list sources using the v1 API func TestListSourcesV1(t *testing.T) { sources := []string{`{ - "name": "package-repo-1", + "id": "package-repo-1", + "name": "First test package repo", "url": "file://REPO-PATH", "type": "yum-baseurl", "proxy": "https://proxy-url/", @@ -186,7 +283,8 @@ func TestListSourcesV1(t *testing.T) { "gpgkey_urls": ["https://url/path/to/gpg-key"] }`, `{ - "name": "package-repo-2", + "id": "package-repo-2", + "name": "Second test package repo", "url": "file://REPO-PATH", "type": "yum-baseurl", "proxy": "https://proxy-url/", @@ -195,10 +293,9 @@ func TestListSourcesV1(t *testing.T) { "gpgkey_urls": ["https://url/path/to/gpg-key"] }`} - // TODO update for PostJSONSourceV1 for i := range sources { source := strings.Replace(sources[i], "REPO-PATH", testState.repoDir, 1) - resp, err := PostJSONSourceV0(testState.socket, source) + resp, err := PostJSONSourceV1(testState.socket, source) require.NoError(t, err, "POST source failed with a client error") require.True(t, resp.Status, "POST source failed: %#v", resp) } @@ -251,7 +348,7 @@ func TestGetSourceInfoV0(t *testing.T) { require.True(t, resp.Status, "DELETE source failed: %#v", resp) } -func UploadUserDefinedSources(t *testing.T, sources []string) { +func UploadUserDefinedSourcesV0(t *testing.T, sources []string) { for i := range sources { source := strings.Replace(sources[i], "REPO-PATH", testState.repoDir, 1) resp, err := PostJSONSourceV0(testState.socket, source) @@ -260,8 +357,17 @@ func UploadUserDefinedSources(t *testing.T, sources []string) { } } +func UploadUserDefinedSourcesV1(t *testing.T, sources []string) { + for i := range sources { + source := strings.Replace(sources[i], "REPO-PATH", testState.repoDir, 1) + resp, err := PostJSONSourceV1(testState.socket, source) + require.NoError(t, err, "POST source failed with a client error") + require.True(t, resp.Status, "POST source failed: %#v", resp) + } +} + // verify user defined sources are not present -func VerifyNoUserDefinedSources(t *testing.T, source_names []string) { +func VerifyNoUserDefinedSourcesV0(t *testing.T, source_names []string) { list, api, err := ListSourcesV0(testState.socket) require.NoError(t, err, "GET source failed with a client error") require.Nil(t, api, "ListSources failed: %#v", api) @@ -271,6 +377,17 @@ func VerifyNoUserDefinedSources(t *testing.T, source_names []string) { } } +// verify user defined sources are not present +func VerifyNoUserDefinedSourcesV1(t *testing.T, source_names []string) { + list, api, err := ListSourcesV1(testState.socket) + require.NoError(t, err, "GET source failed with a client error") + require.Nil(t, api, "ListSources failed: %#v", api) + require.GreaterOrEqual(t, len(list), 1, "Not enough sources returned") + for i := range source_names { + require.NotContains(t, list, source_names[i]) + } +} + func TestDeleteUserDefinedSourcesV0(t *testing.T) { source_names := []string{"package-repo-1", "package-repo-2"} sources := []string{`{ @@ -293,10 +410,10 @@ func TestDeleteUserDefinedSourcesV0(t *testing.T) { }`} // verify test starts without user defined sources - VerifyNoUserDefinedSources(t, source_names) + VerifyNoUserDefinedSourcesV0(t, source_names) // post user defined sources - UploadUserDefinedSources(t, sources) + UploadUserDefinedSourcesV0(t, sources) // note: not verifying user defined sources have been pushed b/c correct // operation of PostJSONSourceV0 is validated in the test functions above @@ -308,7 +425,50 @@ func TestDeleteUserDefinedSourcesV0(t *testing.T) { } // verify removed sources are not present after removal - VerifyNoUserDefinedSources(t, source_names) + VerifyNoUserDefinedSourcesV0(t, source_names) +} + +func TestDeleteUserDefinedSourcesV1(t *testing.T) { + source_names := []string{"package-repo-1", "package-repo-2"} + sources := []string{`{ + "id": "package-repo-1", + "name": "First test package repo", + "url": "file://REPO-PATH", + "type": "yum-baseurl", + "proxy": "https://proxy-url/", + "check_ssl": true, + "check_gpg": true, + "gpgkey_urls": ["https://url/path/to/gpg-key"] + }`, + `{ + "id": "package-repo-2", + "name": "Second test package repo", + "url": "file://REPO-PATH", + "type": "yum-baseurl", + "proxy": "https://proxy-url/", + "check_ssl": true, + "check_gpg": true, + "gpgkey_urls": ["https://url/path/to/gpg-key"] + }`} + + // verify test starts without user defined sources + VerifyNoUserDefinedSourcesV1(t, source_names) + + // post user defined sources + UploadUserDefinedSourcesV1(t, sources) + // note: not verifying user defined sources have been pushed b/c correct + // operation of PostJSONSourceV0 is validated in the test functions above + + // TODO update for DeleteJSONSourceV1 + // Remove the test sources + for _, n := range source_names { + resp, err := DeleteSourceV0(testState.socket, n) + require.NoError(t, err, "DELETE source failed with a client error") + require.True(t, resp.Status, "DELETE source failed: %#v", resp) + } + + // verify removed sources are not present after removal + VerifyNoUserDefinedSourcesV0(t, source_names) } func Index(vs []string, t string) int { diff --git a/internal/store/store.go b/internal/store/store.go index 4125252b6..fae05ff4c 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -407,18 +407,20 @@ func (s *Store) DeleteCompose(id uuid.UUID) error { }) } -func (s *Store) PushSource(source SourceConfig) { +// PushSource stores a SourceConfig in store.Sources +func (s *Store) PushSource(key string, source SourceConfig) { // FIXME: handle or comment this possible error _ = s.change(func() error { - s.sources[source.Name] = source + s.sources[key] = source return nil }) } -func (s *Store) DeleteSource(name string) { +// DeleteSource removes a SourceConfig from store.Sources +func (s *Store) DeleteSource(key string) { // FIXME: handle or comment this possible error _ = s.change(func() error { - delete(s.sources, name) + delete(s.sources, key) return nil }) } diff --git a/internal/weldr/api.go b/internal/weldr/api.go index 409fa96e6..c54d8506f 100644 --- a/internal/weldr/api.go +++ b/internal/weldr/api.go @@ -462,6 +462,35 @@ func (api *API) sourceInfoHandler(writer http.ResponseWriter, request *http.Requ } } +// DecodeSourceConfigV0 parses a request.Body into a SourceConfigV0 +func DecodeSourceConfigV0(body io.Reader, contentType string) (source SourceConfigV0, err error) { + if contentType == "application/json" { + err = json.NewDecoder(body).Decode(&source) + } else if contentType == "text/x-toml" { + _, err = toml.DecodeReader(body, &source) + } else { + err = errors_package.New("blueprint must be in json or toml format") + } + return source, err +} + +// DecodeSourceConfigV1 parses a request.Body into a SourceConfigV1 +func DecodeSourceConfigV1(body io.Reader, contentType string) (source SourceConfigV1, err error) { + if contentType == "application/json" { + err = json.NewDecoder(body).Decode(&source) + } else if contentType == "text/x-toml" { + _, err = toml.DecodeReader(body, &source) + } else { + err = errors_package.New("blueprint must be in json or toml format") + } + + if err == nil && len(source.GetKey()) == 0 { + err = errors_package.New("'id' field is missing from request") + } + + return source, err +} + func (api *API) sourceNewHandler(writer http.ResponseWriter, request *http.Request, params httprouter.Params) { if !verifyRequestVersion(writer, params, 0) { // TODO: version 1 API return @@ -486,22 +515,20 @@ func (api *API) sourceNewHandler(writer http.ResponseWriter, request *http.Reque return } - var source SourceConfigV0 + var source SourceConfig var err error - if contentType[0] == "application/json" { - err = json.NewDecoder(request.Body).Decode(&source) - } else if contentType[0] == "text/x-toml" { - _, err = toml.DecodeReader(request.Body, &source) + if isRequestVersionAtLeast(params, 1) { + source, err = DecodeSourceConfigV1(request.Body, contentType[0]) } else { - err = errors_package.New("blueprint must be in json or toml format") + source, err = DecodeSourceConfigV0(request.Body, contentType[0]) } // Basic check of the source, should at least have a name and type if err == nil { - if len(source.Name) == 0 { - err = errors_package.New("'name' field is missing from API v0 request") - } else if len(source.Type) == 0 { - err = errors_package.New("'type' field is missing from API v0 request") + if len(source.GetName()) == 0 { + err = errors_package.New("'name' field is missing from request") + } else if len(source.GetType()) == 0 { + err = errors_package.New("'type' field is missing from request") } } @@ -514,7 +541,7 @@ func (api *API) sourceNewHandler(writer http.ResponseWriter, request *http.Reque return } - api.store.PushSource(source.SourceConfig()) + api.store.PushSource(source.GetKey(), source.SourceConfig()) statusResponseOK(writer) } diff --git a/internal/weldr/json.go b/internal/weldr/json.go index 272fc02fa..891fc299b 100644 --- a/internal/weldr/json.go +++ b/internal/weldr/json.go @@ -106,6 +106,13 @@ func (s *SourceInfoV0) SourceConfig(sourceName string) (ssc store.SourceConfig, return si.SourceConfig(), true } +type SourceConfig interface { + GetKey() string + GetName() string + GetType() string + SourceConfig() store.SourceConfig +} + // SourceConfigV0 holds the source repository information type SourceConfigV0 struct { Name string `json:"name" toml:"name"` @@ -118,9 +125,64 @@ type SourceConfigV0 struct { GPGUrls []string `json:"gpgkey_urls" toml:"gpgkey_urls"` } +// Key return the key, .Name in this case +func (s SourceConfigV0) GetKey() string { + return s.Name +} + +// Name return the .Name field +func (s SourceConfigV0) GetName() string { + return s.Name +} + +// Type return the .Type field +func (s SourceConfigV0) GetType() string { + return s.Type +} + // SourceConfig returns a SourceConfig struct populated with the supported variables // The store does not support proxy and gpgkey_urls -func (s *SourceConfigV0) SourceConfig() (ssc store.SourceConfig) { +func (s SourceConfigV0) SourceConfig() (ssc store.SourceConfig) { + ssc.Name = s.Name + ssc.Type = s.Type + ssc.URL = s.URL + ssc.CheckGPG = s.CheckGPG + ssc.CheckSSL = s.CheckSSL + + return ssc +} + +// SourceConfigV1 holds the source repository information +type SourceConfigV1 struct { + ID string `json:"id" toml:"id"` + Name string `json:"name" toml:"name"` + Type string `json:"type" toml:"type"` + URL string `json:"url" toml:"url"` + CheckGPG bool `json:"check_gpg" toml:"check_gpg"` + CheckSSL bool `json:"check_ssl" toml:"check_ssl"` + System bool `json:"system" toml:"system"` + Proxy string `json:"proxy" toml:"proxy"` + GPGUrls []string `json:"gpgkey_urls" toml:"gpgkey_urls"` +} + +// Key returns the key, .ID in this case +func (s SourceConfigV1) GetKey() string { + return s.ID +} + +// Name return the .Name field +func (s SourceConfigV1) GetName() string { + return s.Name +} + +// Type return the .Type field +func (s SourceConfigV1) GetType() string { + return s.Type +} + +// SourceConfig returns a SourceConfig struct populated with the supported variables +// The store does not support proxy and gpgkey_urls +func (s SourceConfigV1) SourceConfig() (ssc store.SourceConfig) { ssc.Name = s.Name ssc.Type = s.Type ssc.URL = s.URL