Skip to content

Commit

Permalink
Implement product activation and extension traversel in the public
Browse files Browse the repository at this point in the history
library
  • Loading branch information
felixsch committed Jan 2, 2025
1 parent a6f08bb commit 4c38f0e
Show file tree
Hide file tree
Showing 8 changed files with 502 additions and 14 deletions.
39 changes: 34 additions & 5 deletions cmd/public-api-test/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func waitForUser(message string) {
}
}

func runDemo(regcode string) error {
func runDemo(identifier, version, arch, regcode string) error {
opts := connection.DefaultOptions("public-api-demo", "1.0", "DE")

if url := os.Getenv("SCC_URL"); url != "" {
Expand Down Expand Up @@ -58,10 +58,16 @@ func runDemo(regcode string) error {
return regErr
}
bold("!! check https://scc.suse.com/systems/%d\n", id)
fmt.Println("Note: Unless you activate something on the system we do NOT it in WEB UI")

bold("3) Activate %s/%s/%s\n", identifier, version, arch)
_, root, rootErr := registration.Activate(conn, identifier, version, arch, regcode)
if rootErr != nil {
return rootErr
}
bold("++ %s activated\n", root.FriendlyName)
waitForUser("Registration complete")

bold("3) System status // Ping\n")
bold("4) System status // Ping\n")
systemInformation := map[string]string{
"uname": "public api demo - ping",
}
Expand All @@ -76,7 +82,30 @@ func runDemo(regcode string) error {
}
waitForUser("System update complete")

bold("4) Deregistration of the client\n")
bold("5) Activate recommended extensions/modules\n")
product, prodErr := registration.FetchProductInfo(conn, identifier, version, arch)
if prodErr != nil {
return prodErr
}

activator := func(ext registration.Product) (bool, error) {
if ext.Free && ext.Recommended {
_, act, activateErr := registration.Activate(conn, ext.Identifier, ext.Version, ext.Arch, "")
if activateErr != nil {
return false, activateErr
}
bold("++ %s activated\n", act.FriendlyName)
return true, nil
}
return false, nil
}

if err := product.TraverseExtensions(activator); err != nil {
return err
}
waitForUser("System fully activated")

bold("6) Deregistration of the client\n")
if err := registration.Deregister(conn); err != nil {
return err
}
Expand All @@ -98,7 +127,7 @@ func main() {
os.Exit(1)
}

err := runDemo(regcode)
err := runDemo(os.Args[1], os.Args[2], os.Args[3], regcode)

if err != nil {
fmt.Printf("ERROR: %s\n", err)
Expand Down
100 changes: 96 additions & 4 deletions pkg/registration/activate.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package registration

import "github.com/SUSE/connect-ng/pkg/connection"
import (
"encoding/json"

"github.com/SUSE/connect-ng/pkg/connection"
)

// Metadata holds all the data that is returned by activate/deactivate API calls
// which is not exactly tied to the Product struct. Note that by pairing a
Expand All @@ -21,16 +25,104 @@ type Metadata struct {
ObsoletedName string `json:"obsoleted_service_name"`
}

type activateRequest struct {
Identifier string `json:"identifier"`
Version string `json:"version"`
Arch string `json:"arch"`
Regcode string `json:"token,omitempty"`
}

type activateResponse struct {
ID int `json:"id"`
URL string `json:"url"`
Name string `json:"name"`
ObsoletedName string `json:"obsoleted_service_name"`
Product Product `json:"product"`
}

// Activate a product by pairing an authorized connection (which contains the
// system at hand), plus the "triplet" being used to identify the desired
// product.
func Activate(conn connection.Connection, identifier, version, arch string) (*Metadata, *Product, error) {
return &Metadata{}, &Product{}, nil
func Activate(conn connection.Connection, identifier, version, arch, regcode string) (*Metadata, *Product, error) {
payload := activateRequest{
Identifier: identifier,
Version: version,
Arch: arch,
Regcode: regcode,
}
creds := conn.GetCredentials()
login, password, credErr := creds.Login()

if credErr != nil {
return nil, nil, credErr
}

request, buildErr := conn.BuildRequest("POST", "/connect/systems/products", payload)
if buildErr != nil {
return nil, nil, buildErr
}

connection.AuthBySystemCredentials(request, login, password)

_, response, doErr := conn.Do(request)
if doErr != nil {
return nil, nil, doErr
}

activation := activateResponse{}
if err := json.Unmarshal(response, &activation); err != nil {
return nil, nil, err
}

meta := Metadata{
ID: activation.ID,
URL: activation.URL,
Name: activation.Name,
ObsoletedName: activation.ObsoletedName,
}

return &meta, &activation.Product, nil
}

// Deactivate a product by pairing an authorized connection (which contains the
// system at hand), plus the "triplet" being used to identify the product to be
// deactivated for the system.
func Deactivate(conn connection.Connection, identifier, version, arch string) (*Metadata, *Product, error) {
return &Metadata{}, &Product{}, nil
payload := activateRequest{
Identifier: identifier,
Version: version,
Arch: arch,
}
creds := conn.GetCredentials()
login, password, credErr := creds.Login()

if credErr != nil {
return nil, nil, credErr
}

request, buildErr := conn.BuildRequest("DELETE", "/connect/systems/products", payload)
if buildErr != nil {
return nil, nil, buildErr
}

connection.AuthBySystemCredentials(request, login, password)

_, response, doErr := conn.Do(request)
if doErr != nil {
return nil, nil, doErr
}

deactivation := activateResponse{}
if err := json.Unmarshal(response, &deactivation); err != nil {
return nil, nil, err
}

meta := Metadata{
ID: deactivation.ID,
URL: deactivation.URL,
Name: deactivation.Name,
ObsoletedName: deactivation.ObsoletedName,
}

return &meta, &deactivation.Product, nil
}
66 changes: 66 additions & 0 deletions pkg/registration/activate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package registration_test

import (
"errors"
"testing"

"github.com/SUSE/connect-ng/pkg/registration"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

func TestActivateProductSuccess(t *testing.T) {
assert := assert.New(t)

conn, _ := mockConnectionWithCredentials()

// 204 No Content
payload := fixture(t, "pkg/registration/activate_success.json")
conn.On("Do", mock.Anything).Return(200, payload, nil)

metadata, product, err := registration.Activate(conn, "SLES", "12.1", "x86_64", "regcode")
assert.NoError(err)

assert.Equal("SUSE_Linux_Enterprise_Server_12_x86_64", metadata.ObsoletedName)
assert.Equal("SUSE Linux Enterprise Server 12 x86_64", product.FriendlyName)
}

func TestActivateProductInvalidRegcode(t *testing.T) {
assert := assert.New(t)

conn, _ := mockConnectionWithCredentials()

// 204 No Content
conn.On("Do", mock.Anything).Return(422, []byte{}, errors.New("No valid subscription found"))

_, _, err := registration.Activate(conn, "SLES", "12.1", "x86_64", "regcode")
assert.Error(err)
}

func TestDeactivateProductSuccess(t *testing.T) {
assert := assert.New(t)

conn, _ := mockConnectionWithCredentials()

// 204 No Content
payload := fixture(t, "pkg/registration/deactivate_success.json")
conn.On("Do", mock.Anything).Return(200, payload, nil)

metadata, product, err := registration.Deactivate(conn, "SLES", "12.1", "x86_64")
assert.NoError(err)

assert.Equal("SUSE_Linux_Enterprise_Server_12_x86_64", metadata.ObsoletedName)
assert.Equal("SUSE Linux Enterprise Server 12 x86_64", product.FriendlyName)
}

func TestDeactivateProductInvalidProduct(t *testing.T) {
assert := assert.New(t)

conn, _ := mockConnectionWithCredentials()

// 204 No Content
conn.On("Do", mock.Anything).Return(422, []byte{}, errors.New("Product is a base product and cannot be deactivated"))

_, _, err := registration.Deactivate(conn, "SLES", "12.1", "x86_64")
assert.Error(err)
}
80 changes: 75 additions & 5 deletions pkg/registration/product.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package registration

import (
"encoding/json"

"github.com/SUSE/connect-ng/pkg/connection"
)

// Product as defined from SCC'S API.
type Product struct {
Name string `json:"identifier"`
Version string `json:"version"`
Arch string `json:"arch"`
Summary string `json:"summary,omitempty"`
IsBase bool `json:"isbase"`
Name string `json:"name"`
Identifier string `json:"identifier"`
Version string `json:"version"`
Arch string `json:"arch"`
Summary string `json:"summary,omitempty"`
IsBase bool `json:"isbase"`

FriendlyName string `json:"friendly_name,omitempty"`
ReleaseType string `json:"release_type,omitempty"`
Expand All @@ -26,3 +33,66 @@ type Product struct {
ReleaseStage string `json:"release_stage,omitempty"`
Repositories []Repository `json:"repositories,omitempty"`
}

// TraverseFunc is called for each extension of the given product.
// If true is returned, traversal is continued.
type TraverseFunc func(product Product) (bool, error)

// TraverseExtensions traverse through the products extensions and theirs extensions.
// When TraverseFunc returns false, the full product and its extensions are skipped.
func (pro Product) TraverseExtensions(fn TraverseFunc) error {
for _, extension := range pro.Extensions {
doContinue, fnErr := fn(extension)

if fnErr != nil {
return fnErr
}

if doContinue {
if err := extension.TraverseExtensions(fn); err != nil {
return err
}
}
}
return nil
}

type productShowRequest struct {
Identifier string `json:"identifier"`
Version string `json:"version"`
Arch string `json:"arch"`
}

func FetchProductInfo(conn connection.Connection, identifier, version, arch string) (*Product, error) {
payload := productShowRequest{
Identifier: identifier,
Version: version,
Arch: arch,
}
creds := conn.GetCredentials()
login, password, credErr := creds.Login()

if credErr != nil {
return nil, credErr
}

request, buildErr := conn.BuildRequest("GET", "/connect/systems/products", payload)
if buildErr != nil {
return nil, buildErr
}

connection.AuthBySystemCredentials(request, login, password)

_, response, doErr := conn.Do(request)
if doErr != nil {
return nil, doErr
}

product := Product{}

if err := json.Unmarshal(response, &product); err != nil {
return nil, err
}

return &product, nil
}
Loading

0 comments on commit 4c38f0e

Please sign in to comment.