Repository: osbuild/osbuild-composer
Pull request #3240 (2023-01-25 00:15:47)

cloudapi: Add subscription option for rhc 
This pull request includes:

- [x] adequate testing for the new functionality or fixed issue
- [ ] adequate documentation informing people about the change such as
  - [ ] submit a PR for the [guides](https://github.com/osbuild/guides) repository if this PR changed any behavior described there: https://www.osbuild.org/guides/

<!--
Thanks for proposing a change to osbuild-composer!

Please don't remove the above check list. These are things that each pull
request must have before it is merged. It helps maintainers to not forget
anything.

If the reason for ticking any of the boxes is ambiguous, please add a short
note explaining why.

In addition, if this pull request fixes a downstream issue, please refer to
test/README.md and add these additional items:

- [ ] 1st commit of any `rhbz#` related PR contains bug reproducer; CI reports FAIL or
- [ ] PR contains automated tests for new functionality and
- [ ] QE has approved reproducer/new tests and
- [ ] Subsequent commits provide bug fixes without modifying the reproducer; CI reports PASS and
- [ ] QE approves this PR; RHBZ status is set to `MODIFIED + Verified=Tested`

Information regarding our GitLab pipeline (Schutzbot):

CI will not be ran automatically if WIP label is applied or the PR is in DRAFT state, instead only
a link will be provided to the pipeline which can then be triggered manually if desired. To run the
CI automatically either switch the PR to ready or apply WIP+test label.

Outside contributors need manual approval from one of the osbuild-composer maintainers.

Schutzbot will only be triggered if all Tests jobs in GitHub workflow succeed.
-->


bcl: > Currently we can specify insights true/false, I'd add something similar for rhc.

Sure that can be done as well, although the jira issue (HMS-198) made it sound like this was a replacement and that it should only use subscription manager if insights wasn't selected. It's also confusing that rhc remediation requires yet another thing (smart management subscription) that we're not setting up.
teg: @croissanne should it be an enum rather than two bools? That way we can make sure we only expose combinations that make sense?

I'm not really sure why we would use insight client at all, did we get that confirmed from PM?
bcl: Any suggestions for adding testing for this? 
croissanne: > Any suggestions for adding testing for this?

We could add it to the subscription block:
https://github.com/osbuild/osbuild-composer/blob/356f261bcd9f3e36b9109e9c82b8c3c9d3875ac3/test/cases/api.sh#L317-L331

And then verify/call `rhc` in instancecheck:
https://github.com/osbuild/osbuild-composer/blob/356f261bcd9f3e36b9109e9c82b8c3c9d3875ac3/test/cases/api/common/common.sh#L18-L46
bcl: I'd rather test things sooner than later, so I figured out how to add some tests to manifest (which looks like it could use quite a bit more testing coverage). It may also be useful to add something to api.sh but it doesn't look very flexible. We would want to test all  4 subscription states and it looks like it can only handle one.

achilleas-k: > > @croissanne should it be an enum rather than two bools? That way we can make sure we only expose combinations that make sense?
> > I'm not really sure why we would use insight client at all, did we get that confirmed from PM?
> 
> rhc will automatically enable insights-client for you. An enum would make more sense but unsure how to not break the api here.

I've approved but holding off on merging because this topic wasn't finalised.

Patch:
diff --git a/internal/cloudapi/v2/handler.go b/internal/cloudapi/v2/handler.go
index c6b6e58cf..bf30ce74d 100644
--- a/internal/cloudapi/v2/handler.go
+++ b/internal/cloudapi/v2/handler.go
@@ -278,12 +278,18 @@ func (h *apiHandlers) PostCompose(ctx echo.Context) error {
 		}
 
 		if request.Customizations != nil && request.Customizations.Subscription != nil {
+			// Rhc is optional, default to false if not included
+			var rhc bool
+			if request.Customizations.Subscription.Rhc != nil {
+				rhc = *request.Customizations.Subscription.Rhc
+			}
 			imageOptions.Subscription = &distro.SubscriptionImageOptions{
 				Organization:  request.Customizations.Subscription.Organization,
 				ActivationKey: request.Customizations.Subscription.ActivationKey,
 				ServerUrl:     request.Customizations.Subscription.ServerUrl,
 				BaseUrl:       request.Customizations.Subscription.BaseUrl,
 				Insights:      request.Customizations.Subscription.Insights,
+				Rhc:           rhc,
 			}
 		}
 
diff --git a/internal/cloudapi/v2/openapi.v2.gen.go b/internal/cloudapi/v2/openapi.v2.gen.go
index 04d1cb13e..528d6389b 100644
--- a/internal/cloudapi/v2/openapi.v2.gen.go
+++ b/internal/cloudapi/v2/openapi.v2.gen.go
@@ -497,7 +497,10 @@ type Subscription struct {
 	BaseUrl       string `json:"base_url"`
 	Insights      bool   `json:"insights"`
 	Organization  string `json:"organization"`
-	ServerUrl     string `json:"server_url"`
+
+	// Optional flag to use rhc to register the system, which also always enables Insights.
+	Rhc       *bool  `json:"rhc,omitempty"`
+	ServerUrl string `json:"server_url"`
 }
 
 // This should really be oneOf but AWSS3UploadOptions is a subset of
@@ -895,34 +898,35 @@ var swaggerSpec = []string{
 	"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",
-	"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",
+	"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",
+	"o4hIwguPomH2wfn+4/8HAAD//3wEUOAObwAA",
 }
 
 // GetSwagger returns the content of the embedded swagger specification file
diff --git a/internal/cloudapi/v2/openapi.v2.yml b/internal/cloudapi/v2/openapi.v2.yml
index 907b1cd9b..8fe307ff1 100644
--- a/internal/cloudapi/v2/openapi.v2.yml
+++ b/internal/cloudapi/v2/openapi.v2.yml
@@ -1103,6 +1103,12 @@ components:
         insights:
           type: boolean
           example: true
+        rhc:
+          type: boolean
+          default: false
+          example: true
+          description: |
+            Optional flag to use rhc to register the system, which also always enables Insights.
     User:
       type: object
       required:
diff --git a/internal/distro/distro.go b/internal/distro/distro.go
index dcce0bfe8..c40c55186 100644
--- a/internal/distro/distro.go
+++ b/internal/distro/distro.go
@@ -166,6 +166,7 @@ type SubscriptionImageOptions struct {
 	ServerUrl     string
 	BaseUrl       string
 	Insights      bool
+	Rhc           bool
 }
 
 // The FactsImageOptions specify things to be stored into the Insights facts
diff --git a/internal/manifest/os.go b/internal/manifest/os.go
index 4fad5f0f0..6ddf7ad83 100644
--- a/internal/manifest/os.go
+++ b/internal/manifest/os.go
@@ -189,6 +189,18 @@ func (p *OS) getPackageSetChain() []rpmmd.PackageSet {
 		packages = append(packages, "openscap-scanner", "scap-security-guide")
 	}
 
+	// Make sure the right packages are included for subscriptions
+	// rhc always uses insights, and depends on subscription-manager
+	// non-rhc uses subscription-manager and optionally includes Insights
+	if p.Subscription != nil {
+		packages = append(packages, "subscription-manager")
+		if p.Subscription.Rhc {
+			packages = append(packages, "rhc", "insights-client")
+		} else if p.Subscription.Insights {
+			packages = append(packages, "insights-client")
+		}
+	}
+
 	chain := []rpmmd.PackageSet{
 		{
 			Include:      append(packages, p.ExtraBasePackages...),
@@ -480,13 +492,27 @@ func (p *OS) serialize() osbuild.Pipeline {
 		pipeline.AddStage(osbuild.NewPwqualityConfStage(p.PwQuality))
 	}
 
+	// If subscription settings are included there are 3 possible setups:
+	// - Register the system with rhc and enable Insights
+	// - Register with subscription-manager, no Insights or rhc
+	// - Register with subscription-manager and enable Insights, no rhc
 	if p.Subscription != nil {
-		commands := []string{
-			fmt.Sprintf("/usr/sbin/subscription-manager register --org=%s --activationkey=%s --serverurl %s --baseurl %s", p.Subscription.Organization, p.Subscription.ActivationKey, p.Subscription.ServerUrl, p.Subscription.BaseUrl),
-		}
-		if p.Subscription.Insights {
+		var commands []string
+		if p.Subscription.Rhc {
+			// Use rhc for registration instead of subscription manager
+			commands = []string{fmt.Sprintf("/usr/bin/rhc connect -o=%s -a=%s --server %s", p.Subscription.Organization, p.Subscription.ActivationKey, p.Subscription.ServerUrl)}
+
+			// Always enable Insights when using rhc
 			commands = append(commands, "/usr/bin/insights-client --register")
+		} else {
+			commands = []string{fmt.Sprintf("/usr/sbin/subscription-manager register --org=%s --activationkey=%s --serverurl %s --baseurl %s", p.Subscription.Organization, p.Subscription.ActivationKey, p.Subscription.ServerUrl, p.Subscription.BaseUrl)}
+
+			// Insights is optional when using subscription-manager
+			if p.Subscription.Insights {
+				commands = append(commands, "/usr/bin/insights-client --register")
+			}
 		}
+
 		pipeline.AddStage(osbuild.NewFirstBootStage(&osbuild.FirstBootStageOptions{
 			Commands:       commands,
 			WaitForNetwork: true,
diff --git a/internal/prometheus/job_metrics.go b/internal/prometheus/job_metrics.go
index 32f5fddfa..d233983ec 100644
--- a/internal/prometheus/job_metrics.go
+++ b/internal/prometheus/job_metrics.go
@@ -41,7 +41,7 @@ var (
 		Namespace: Namespace,
 		Subsystem: WorkerSubsystem,
 		Help:      "Duration spent by workers on a job.",
-		Buckets:   []float64{.1, .2, .5, 1, 2, 4, 8, 16, 32, 40, 48, 64, 96, 128, 160, 192, 224, 256, 320, 382, 448, 512, 640, 768, 896, 1024, 1280, 1536, 1792, 2049},
+		Buckets:   []float64{.1, .2, .5, 1, 2, 4, 8, 16, 32, 40, 48, 64, 96, 128, 160, 192, 224, 256, 320, 382, 448, 512, 640, 768, 896, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4608, 5120, 5632, 6144, 6656, 7168, 7680, 8192, 8704, 9216, 9728, 10240, 10752},
 	}, []string{"type", "status", "tenant", "arch"})
 )
 
@@ -51,7 +51,7 @@ var (
 		Namespace: Namespace,
 		Subsystem: WorkerSubsystem,
 		Help:      "Duration a job spends on the queue.",
-		Buckets:   []float64{.1, .2, .5, 1, 2, 4, 8, 16, 32, 40, 48, 64, 96, 128, 160, 192, 224, 256, 320, 382, 448, 512, 640, 768, 896, 1024, 1280, 1536, 1792, 2049},
+		Buckets:   []float64{.1, .2, .5, 1, 2, 4, 8, 16, 32, 40, 48, 64, 96, 128, 160, 192, 224, 256, 320, 382, 448, 512, 640, 768, 896, 1024, 1280, 1536, 1792, 2048, 2304, 2560, 2816, 3072, 3328, 3584, 3840, 4096, 4608, 5120, 5632, 6144, 6656, 7168, 7680, 8192, 8704, 9216, 9728, 10240, 10752},
 	}, []string{"type", "tenant"})
 )
 
diff --git a/internal/upload/koji/koji.go b/internal/upload/koji/koji.go
index 084544e7a..ae2cbd9b2 100644
--- a/internal/upload/koji/koji.go
+++ b/internal/upload/koji/koji.go
@@ -478,13 +478,24 @@ func customCheckRetry(ctx context.Context, resp *http.Response, err error) (bool
 		}
 	}
 
+	if !shouldRetry {
+		logrus.Info("Not retrying: ", resp.Status)
+	}
+
 	return shouldRetry, retErr
 }
 
+func logUnsuccessfulResponses(logger rh.Logger, resp *http.Response) {
+	if !(resp.StatusCode >= 200 && resp.StatusCode < 300) {
+		logger.Printf("Unsuccessful request: ", resp)
+	}
+}
+
 func createCustomRetryableClient() *rh.Client {
 	client := rh.NewClient()
 	client.Logger = rh.LeveledLogger(&LeveledLogrus{logrus.StandardLogger()})
 	client.CheckRetry = customCheckRetry
+	client.ResponseLogHook = logUnsuccessfulResponses
 	return client
 }
 
diff --git a/osbuild-composer.spec b/osbuild-composer.spec
index 7ee4be242..edf3adcd8 100644
--- a/osbuild-composer.spec
+++ b/osbuild-composer.spec
@@ -9,7 +9,7 @@
 
 %global goipath         github.com/osbuild/osbuild-composer
 
-Version:        74
+Version:        75
 
 %gometa
 


Test patch:
diff --git a/internal/cloudapi/v2/v2_test.go b/internal/cloudapi/v2/v2_test.go
index 6f1c8267a..9b5c2a07f 100644
--- a/internal/cloudapi/v2/v2_test.go
+++ b/internal/cloudapi/v2/v2_test.go
@@ -947,6 +947,42 @@ func TestComposeCustomizations(t *testing.T) {
 	}`, "id")
 }
 
+func TestComposeRhcSubscription(t *testing.T) {
+	srv, _, _, cancel := newV2Server(t, t.TempDir(), []string{""}, false, false)
+	defer cancel()
+
+	test.TestRoute(t, srv.Handler("/api/image-builder-composer/v2"), false, "POST", "/api/image-builder-composer/v2/compose", fmt.Sprintf(`
+	{
+		"distribution": "%s",
+		"customizations": {
+			"subscription": {
+				"organization": "2040324",
+				"activation_key": "my-secret-key",
+				"server_url": "subscription.rhsm.redhat.com",
+				"base_url": "http://cdn.redhat.com/",
+				"insights": false,
+				"rhc": true
+			},
+			"packages": [ "pkg1", "pkg2" ]
+		},
+		"image_request":{
+			"architecture": "%s",
+			"image_type": "aws",
+			"repositories": [{
+				"baseurl": "somerepo.org",
+				"rhsm": false
+			}],
+			"upload_options": {
+				"region": "eu-central-1"
+			}
+		 }
+	}`, test_distro.TestDistroName, test_distro.TestArch3Name), http.StatusCreated, `
+	{
+		"href": "/api/image-builder-composer/v2/compose",
+		"kind": "ComposeId"
+	}`, "id")
+}
+
 func TestImageTypes(t *testing.T) {
 	srv, _, _, cancel := newV2Server(t, t.TempDir(), []string{""}, false, false)
 	defer cancel()
diff --git a/internal/manifest/os_test.go b/internal/manifest/os_test.go
new file mode 100644
index 000000000..83bbcacd4
--- /dev/null
+++ b/internal/manifest/os_test.go
@@ -0,0 +1,152 @@
+package manifest
+
+import (
+	"testing"
+
+	"github.com/osbuild/osbuild-composer/internal/distro"
+	"github.com/osbuild/osbuild-composer/internal/osbuild"
+	"github.com/osbuild/osbuild-composer/internal/platform"
+	"github.com/osbuild/osbuild-composer/internal/rpmmd"
+	"github.com/osbuild/osbuild-composer/internal/runner"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// NewTestOS returns a minimally populated OS struct for use in testing
+func NewTestOS() *OS {
+	repos := []rpmmd.RepoConfig{}
+	manifest := New()
+	runner := &runner.Fedora{Version: 36}
+	build := NewBuild(&manifest, runner, repos)
+	build.Checkpoint()
+
+	// create an x86_64 platform with bios boot
+	platform := &platform.X86{
+		BIOS: true,
+	}
+
+	os := NewOS(&manifest, build, platform, repos)
+	packages := []rpmmd.PackageSpec{
+		rpmmd.PackageSpec{Name: "pkg1"},
+	}
+	os.serializeStart(packages)
+
+	return os
+}
+
+// CheckFirstBootStageOptions checks the Command strings
+func CheckFirstBootStageOptions(t *testing.T, stages []*osbuild.Stage, commands []string) {
+	// Find the FirstBootStage
+	for _, s := range stages {
+		if s.Type == "org.osbuild.first-boot" {
+			require.NotNil(t, s.Options)
+			options, ok := s.Options.(*osbuild.FirstBootStageOptions)
+			require.True(t, ok)
+			require.Equal(t, len(options.Commands), len(commands))
+
+			// Make sure the commands are the same
+			for idx, cmd := range commands {
+				assert.Equal(t, cmd, options.Commands[idx])
+			}
+		}
+	}
+}
+
+// CheckPkgSetInclude makes sure the packages named in pkgs are all included
+func CheckPkgSetInclude(t *testing.T, pkgSetChain []rpmmd.PackageSet, pkgs []string) {
+
+	// Gather up all the includes
+	var includes []string
+	for _, ps := range pkgSetChain {
+		includes = append(includes, ps.Include...)
+	}
+
+	for _, p := range pkgs {
+		assert.Contains(t, includes, p)
+	}
+}
+
+func TestSubscriptionManagerCommands(t *testing.T) {
+	os := NewTestOS()
+	os.Subscription = &distro.SubscriptionImageOptions{
+		Organization:  "2040324",
+		ActivationKey: "my-secret-key",
+		ServerUrl:     "subscription.rhsm.redhat.com",
+		BaseUrl:       "http://cdn.redhat.com/",
+	}
+	pipeline := os.serialize()
+	CheckFirstBootStageOptions(t, pipeline.Stages, []string{
+		"/usr/sbin/subscription-manager register --org=2040324 --activationkey=my-secret-key --serverurl subscription.rhsm.redhat.com --baseurl http://cdn.redhat.com/",
+	})
+}
+
+func TestSubscriptionManagerInsightsCommands(t *testing.T) {
+	os := NewTestOS()
+	os.Subscription = &distro.SubscriptionImageOptions{
+		Organization:  "2040324",
+		ActivationKey: "my-secret-key",
+		ServerUrl:     "subscription.rhsm.redhat.com",
+		BaseUrl:       "http://cdn.redhat.com/",
+		Insights:      true,
+	}
+	pipeline := os.serialize()
+	CheckFirstBootStageOptions(t, pipeline.Stages, []string{
+		"/usr/sbin/subscription-manager register --org=2040324 --activationkey=my-secret-key --serverurl subscription.rhsm.redhat.com --baseurl http://cdn.redhat.com/",
+		"/usr/bin/insights-client --register",
+	})
+}
+
+func TestRhcInsightsCommands(t *testing.T) {
+	os := NewTestOS()
+	os.Subscription = &distro.SubscriptionImageOptions{
+		Organization:  "2040324",
+		ActivationKey: "my-secret-key",
+		ServerUrl:     "subscription.rhsm.redhat.com",
+		BaseUrl:       "http://cdn.redhat.com/",
+		Insights:      false,
+		Rhc:           true,
+	}
+	pipeline := os.serialize()
+	CheckFirstBootStageOptions(t, pipeline.Stages, []string{
+		"/usr/bin/rhc connect -o=2040324 -a=my-secret-key --server subscription.rhsm.redhat.com",
+		"/usr/bin/insights-client --register",
+	})
+}
+
+func TestSubscriptionManagerPackages(t *testing.T) {
+	os := NewTestOS()
+	os.Subscription = &distro.SubscriptionImageOptions{
+		Organization:  "2040324",
+		ActivationKey: "my-secret-key",
+		ServerUrl:     "subscription.rhsm.redhat.com",
+		BaseUrl:       "http://cdn.redhat.com/",
+	}
+
+	CheckPkgSetInclude(t, os.getPackageSetChain(), []string{"subscription-manager"})
+}
+
+func TestSubscriptionManagerInsightsPackages(t *testing.T) {
+	os := NewTestOS()
+	os.Subscription = &distro.SubscriptionImageOptions{
+		Organization:  "2040324",
+		ActivationKey: "my-secret-key",
+		ServerUrl:     "subscription.rhsm.redhat.com",
+		BaseUrl:       "http://cdn.redhat.com/",
+		Insights:      true,
+	}
+	CheckPkgSetInclude(t, os.getPackageSetChain(), []string{"subscription-manager", "insights-client"})
+}
+
+func TestRhcInsightsPackages(t *testing.T) {
+	os := NewTestOS()
+	os.Subscription = &distro.SubscriptionImageOptions{
+		Organization:  "2040324",
+		ActivationKey: "my-secret-key",
+		ServerUrl:     "subscription.rhsm.redhat.com",
+		BaseUrl:       "http://cdn.redhat.com/",
+		Insights:      false,
+		Rhc:           true,
+	}
+	CheckPkgSetInclude(t, os.getPackageSetChain(), []string{"rhc", "subscription-manager", "insights-client"})
+}