Skip to content

Conversation

@crdant
Copy link
Member

@crdant crdant commented Oct 31, 2025

Adds support for v1beta1 and v1beta2 license formats

TL;DR

Implements comprehensive multi-version license support through LicenseWrapper abstraction, enabling customers to use either v1beta1 or v1beta2 license formats interchangeably across all installation, upgrade, and template rendering operations while maintaining full backwards compatibility.

Depends on replicatedhq/kots#5675

Details

The embedded-cluster codebase previously coupled tightly to the kotsv1beta1.License type, preventing support for newer v1beta2 license formats that include enhanced entitlement structures and metadata. This migration introduces the LicenseWrapper abstraction from kotskinds (commit 174e89c93554), which provides version-agnostic access to license fields and entitlements, allowing the system to transparently handle both license versions without code duplication or conditional logic scattered throughout the codebase.

Architecture Overview

The implementation follows a three-layer approach:

  1. Parsing Layer - Updated pkg/helpers/parse.go to return licensewrapper.LicenseWrapper instead of raw kotsv1beta1.License types, with LoadLicenseFromBytes() handling version detection and appropriate struct initialization.

  2. Business Logic Layer - Migrated all production code across CLI commands (cmd/installer/cli/), infrastructure managers (api/internal/managers/), and template engine (api/pkg/template/) to use LicenseWrapper types exclusively, replacing direct struct field access with wrapper methods.

  3. API Layer - Replaced direct .Spec.* field access patterns with wrapper getter methods like GetAppSlug(), GetLicenseID(), GetChannelName(), and IsEmbeddedClusterMultiNodeEnabled(), ensuring consistent behavior regardless of underlying license version.

Key Changes by Component

Parser Infrastructure (pkg/helpers/)

  • ParseLicense now returns licensewrapper.LicenseWrapper with automatic version detection
  • ParseLicenseFromBytes delegates to licensewrapper.LoadLicenseFromBytes() for both v1beta1 and v1beta2 parsing
  • Comprehensive test coverage with fixtures for both versions, invalid versions, and edge cases like missing fields

Installer CLI (cmd/installer/cli/)

  • Install command updated to use LicenseWrapper throughout configuration, validation, and metrics reporting
  • Upgrade command migrated from *kotsv1beta1.License to licensewrapper.LicenseWrapper in upgradeConfig struct
  • License expiration validation refactored to use entitlement abstraction methods instead of direct .StrVal access
  • All metrics reporters updated to use wrapper methods for license ID and app slug extraction

Infrastructure Managers (api/internal/managers/)

  • Linux and Kubernetes install managers updated to accept and process LicenseWrapper types
  • Upgrade manager parsing refactored to use LoadLicenseFromBytes() instead of raw unmarshaling
  • LicenseInfo population in Installation CRD updated to call IsDisasterRecoverySupported() and IsEmbeddedClusterMultiNodeEnabled() wrapper methods
  • Resolved critical merge conflict with duplicate license field definition in app config manager

Template Engine (api/pkg/template/)

  • Engine struct updated to store licensewrapper.LicenseWrapper instead of raw license pointer
  • All license template functions refactored to use wrapper getter methods for consistent field access
  • LicenseFieldValue function updated to use Value() method on entitlement fields, supporting both v1beta1 direct values and v1beta2 structured entitlements
  • Channel name resolution updated to use GetChannels(), GetChannelID(), and GetChannelName() methods
  • Nil safety improved with direct V1/V2 checks instead of method calls where appropriate

Package-Level Types (pkg/)

  • Metrics reporter updated to accept LicenseWrapper and use getter methods
  • Kubeutils installation functions migrated to LicenseWrapper parameters
  • Addons installation updated to use wrapper methods for license field access

Validation and Testing

The implementation includes extensive test coverage:

  • TDD approach with test fixtures created before implementation (pkg/helpers/testdata/)
  • Tests for v1beta1 license parsing, v1beta2 license parsing, invalid versions, missing required fields
  • Integration tests updated with apiVersion/kind metadata for LicenseWrapper compatibility
  • CLI tests refactored to use LicenseWrapper constructors and validation methods
  • Template engine tests covering both license versions with all template functions
  • All production code paths verified to use wrapper methods exclusively

Test results confirm successful migration:

  • ✅ Parser tests: PASS (pkg/helpers)
  • ✅ Template engine tests: PASS (api/pkg/template)
  • ✅ Infrastructure manager tests: PASS (api/internal/managers)
  • ✅ Build succeeds: go build ./...
  • ✅ No direct .Spec.* license access in production code
  • ✅ No old *kotsv1beta1.License types in production code

Backwards Compatibility

Existing v1beta1 licenses continue to work without any changes:

  • LicenseWrapper detects version from apiVersion field and populates appropriate internal struct (V1 or V2)
  • Getter methods provide identical return values regardless of version
  • License storage remains byte-based with no schema migrations required
  • Template syntax unchanged, with all functions working transparently across versions

Breaking Changes

None. This is a purely internal refactoring that maintains API compatibility:

  • CLI flags and arguments unchanged
  • Template function signatures identical
  • License file formats both supported
  • Metrics and reporting behavior preserved

Impact

Customers can now:

  • Use v1beta2 licenses with enhanced entitlement metadata and structured fields
  • Continue using existing v1beta1 licenses without modification
  • Switch between license versions seamlessly during upgrades
  • Access new entitlement features when available in v1beta2 format

The system maintains:

  • Full backwards compatibility with all existing v1beta1 licenses
  • Consistent behavior across installation, upgrade, and template rendering paths
  • Type safety through wrapper methods rather than direct field access
  • Extensibility for future license versions through abstraction layer

Dependency Update

Updated kotskinds to commit 174e89c93554 which includes:

  • LicenseWrapper struct with V1 and V2 fields
  • Version-agnostic getter methods for all common license fields
  • LoadLicenseFromBytes function with automatic version detection
  • EntitlementFieldWrapper for abstracted entitlement value access

Related Documentation

  • Implementation Plan: docs/plans/2025-10-31-complete-licensewrapper-migration.md
  • Research Notes: docs/research/2025-10-31-license-wrapper-remaining-work.md
  • Test Fixtures: pkg/helpers/testdata/license-*.yaml

🤖 Generated with Claude Code

crdant and others added 21 commits October 30, 2025 21:19
Creates comprehensive test fixtures for KOTS license validation testing as part
of implementing v1beta2 license support using TDD methodology. These fixtures
enable testing before implementation.

Fixtures include:
- license-v1beta1.yaml: Valid v1beta1 license for backward compatibility testing
- license-v1beta2.yaml: Valid v1beta2 license with signature v2 format
- license-v1beta2-missing-appslug.yaml: Invalid license missing required appSlug
- license-v1beta2-no-ec-enabled.yaml: Invalid license with EC disabled
- license-invalid-version.yaml: Invalid license with unsupported v1beta3 version

These fixtures will be used in upcoming tests for license validation, version
detection, and error handling before implementing the actual license helper
functions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Updates kotskinds dependency to include LicenseWrapper support for handling
both v1beta1 and v1beta2 License CRDs. This enables version-agnostic license
parsing required for supporting multiple license API versions.

Also includes transitive dependency updates to controller-runtime v0.22.3
and protobuf v1.36.8.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Updates test fixtures to use simpler entitlement structure that matches the
format used in actual KOTS licenses, making tests more realistic and easier
to maintain.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Adds comprehensive test coverage for ParseLicense() and ParseLicenseFromBytes()
with both v1beta1 and v1beta2 license formats. Tests verify:
- Version detection (IsV1/IsV2)
- Common field access through LicenseWrapper
- Error handling for invalid versions and malformed YAML
- File-based and byte-based parsing

These tests drive the implementation changes in the next commit (TDD red phase).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…support

Updates ParseLicense() to return licensewrapper.LicenseWrapper instead of
*kotsv1beta1.License, enabling version-agnostic handling of both v1beta1 and
v1beta2 licenses.

Changes:
- ParseLicense() now returns LicenseWrapper with version detection
- New ParseLicenseFromBytes() for parsing from byte arrays
- Leverages kotskinds licensewrapper.LoadLicenseFromBytes() for automatic
  version detection and unified access patterns

This completes Phase 1 of the v1beta2 license migration (TDD green phase).
All tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…bstraction

Updates the metrics reporter to use the LicenseWrapper type instead of direct
*kotsv1beta1.License pointers. This change supports the multi-version license
abstraction layer, allowing the metrics system to work with both v1beta1 and
v1beta2 licenses through a unified interface.

Changes:
- LicenseID() now accepts LicenseWrapper and uses GetLicenseID() method
- License() now returns LicenseWrapper instead of *kotsv1beta1.License
- Removed nil checks as empty wrapper is safer than nil pointer

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…n license support

Migrates the installer CLI commands to use LicenseWrapper instead of direct
*kotsv1beta1.License pointers. This enables the installer to work with both
v1beta1 and v1beta2 license formats through a unified abstraction layer.

Changes to install.go:
- installConfig.license field now uses LicenseWrapper type
- getLicenseFromFilepath() returns LicenseWrapper
- All license field accesses updated to use wrapper methods (GetLicenseID,
  GetAppSlug, GetChannelID, GetChannelName, etc.)
- checkChannelExistence() accepts LicenseWrapper parameter
- maybePromptForAppUpdate() uses wrapper methods for license checks
- printSuccessMessage() updated to work with LicenseWrapper

Changes to release.go:
- getCurrentAppChannelRelease() accepts LicenseWrapper parameter
- License ID access updated to use GetLicenseID() method

Note: This commit still has 2 compilation errors remaining that require Phase 4
changes to addons.InstallOptions and kubeutils.RecordInstallationOptions structs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…e abstraction

Updates core package types to use licensewrapper.LicenseWrapper instead of
*kotsv1beta1.License to support multiple license API versions (v1beta1 and v1beta2).

Changes:
- pkg/addons: Updates InstallOptions and KubernetesInstallOptions License field
- pkg/kubeutils: Updates RecordInstallationOptions License field and calls to
  wrapper methods (IsDisasterRecoverySupported(), IsEmbeddedClusterMultiNodeEnabled())

This enables transparent handling of both v1beta1 and v1beta2 licenses throughout
the addon installation and Kubernetes utility layers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…lti-version support

Completes the migration to licensewrapper.LicenseWrapper in infrastructure managers,
enabling support for both v1beta1 and v1beta2 license API versions.

Changes:
- api/internal/managers/kubernetes/infra: Updates all internal functions to accept
  LicenseWrapper, switches to helpers.ParseLicenseFromBytes() for parsing, updates
  field accesses to use wrapper methods
- api/internal/managers/linux/infra: Updates all internal functions to accept
  LicenseWrapper, switches to helpers.ParseLicenseFromBytes() for parsing, updates
  field accesses to use wrapper methods

Result: Project now compiles successfully with full LicenseWrapper support across
all major components (CLI, metrics, packages, and managers).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…ion support

This commit refactors the template engine and related components to use
LicenseWrapper, enabling support for both v1beta1 and v1beta2 license
formats:

- Template engine now accepts LicenseWrapper instead of concrete v1beta1
  License type
- All license field access goes through wrapper methods (GetAppSlug,
  GetLicenseID, IsSnapshotSupported, etc.)
- App config and release managers updated to accept LicenseWrapper
  parameters
- Install controller now uses helpers.ParseLicenseFromBytes for license
  parsing
- Added comprehensive tests with wrapper helpers for both v1beta1 and
  v1beta2
- Tests verify backward compatibility with v1beta1 while supporting
  v1beta2

This change maintains backward compatibility while enabling the system to
work with both license versions through the abstraction layer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…e support

Updates installer CLI test files to work with the LicenseWrapper abstraction
introduced in previous refactoring commits. Changes include:

- Wraps v1beta1 license objects in LicenseWrapper before passing to updated
  functions (maybePromptForAppUpdate, getCurrentAppChannelRelease)
- Adds required apiVersion and kind headers to all test license YAML strings
  to satisfy licensewrapper.LoadLicenseFromPath validation requirements
- Imports licensewrapper package in both test files

These changes ensure all CLI tests pass with the new multi-version license
support while maintaining existing test coverage and behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…er compatibility

After code review, several test files needed updates to provide proper
Kubernetes resource format (with apiVersion and kind) for license test data,
as licensewrapper.LoadLicenseFromBytes() requires these fields.

Changes:
- api/internal/managers/app/install/install_test.go: Use proper YAML format
- pkg/kubeutils/installation_test.go: Wrap licenses in LicenseWrapper with proper closing braces
- Other test files: Minor formatting improvements

All tests now pass successfully.
Integration tests were creating inline license fixtures without proper
Kubernetes resource headers. licensewrapper.LoadLicenseFromBytes() requires
apiVersion and kind fields for version detection.

Fixed both linux and kubernetes install test fixtures to include:
- apiVersion: kots.io/v1beta1
- kind: License

This ensures integration tests work with the new LicenseWrapper abstraction.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Removes the unused sigs.k8s.io/yaml import that is no longer needed after
migrating to LicenseWrapper. The kyaml package was previously used for
unmarshaling license data, but this is now handled by the
licensewrapper.LoadLicenseFromBytes() function.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Updates the install and upgrade infrastructure managers to use LicenseWrapper
methods instead of directly accessing license Spec fields. Key changes:

- install.go: Fixes IsDisasterRecoverySupported() method call (was missing
  parentheses, treating it as a field instead of a method)
- upgrade.go: Migrates from kyaml.Unmarshal to LoadLicenseFromBytes() for
  license parsing, and updates all license field access to use wrapper
  methods (GetLicenseID(), IsDisasterRecoverySupported(),
  IsEmbeddedClusterMultiNodeEnabled())

This ensures the infrastructure layer works correctly with both v1beta1 and
v1beta2 license formats through the unified wrapper interface.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
… access

Updates the template engine to work correctly with LicenseWrapper:

- Changes nil checks from checking the wrapper directly to using
  GetLicenseID() == "" to determine if license data is present
- Updates channel access to use GetChannels() and GetChannelName() wrapper
  methods instead of direct Spec.Channels access
- Removes unnecessary error returns from licenseFieldValue() for consistency
  with string return type
- Adds missing closing brace in license_test.go
- Wraps test licenses in ChannelName tests to match production usage

These changes ensure templates work with both v1beta1 and v1beta2 licenses
through the unified wrapper interface.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
…ntly

Updates the install and upgrade CLI commands to use LicenseWrapper getter
methods and properly handle entitlement values:

install.go:
- Fixes metrics reporter to use GetLicenseID() and GetAppSlug() instead of
  accessing Spec fields directly
- Fixes addon install options to use installCfg.license instead of
  flags.license for consistency
- Fixes expires_at entitlement parsing to properly extract string value
  using type assertion on Value() interface{} result instead of accessing
  StrVal field directly

upgrade.go:
- Updates license field type from *kotsv1beta1.License to
  licensewrapper.LicenseWrapper
- Updates metrics reporter to use GetLicenseID() and GetAppSlug() wrapper
  methods

These changes complete the CLI migration to LicenseWrapper, enabling support
for both v1beta1 and v1beta2 license formats.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
… calls

When Engine has no license, e.license is a zero-value LicenseWrapper{V1: nil, V2: nil}.
Calling GetLicenseID() on this would panic with nil pointer dereference.
Changed to check e.license.V1 == nil && e.license.V2 == nil directly before accessing.
LicenseFieldValue now returns empty string instead of error when license
or release data is missing. This is more consistent with template behavior
where missing data results in empty strings rather than template errors.

Updated test expectations:
- TestEngine_LicenseFieldValueWithoutLicense: expect empty string
- TestEngine_LicenseFieldValue_EndpointWithoutReleaseData: expect empty string
@crdant crdant changed the title feat: Complete LicenseWrapper migration for v1beta2 license support Support v1beta2 licenses Oct 31, 2025
crdant and others added 5 commits October 31, 2025 12:24
Restore the original behavior where LicenseFieldValue returns an error
when the license is nil, rather than silently returning an empty string.
This maintains backward compatibility with existing code that may depend
on error handling for missing licenses.

Changes:
- Update licenseFieldValue() to return (string, error) instead of string
- Return explicit errors when license or release data is nil
- Update tests to expect errors instead of empty strings

This follows the fail-fast principle where missing critical data should
produce explicit errors rather than silently propagating empty values.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Fix the expires_at entitlement access to properly use the LicenseWrapper
API. The EntitlementField.GetValue() returns an EntitlementValue by value,
and we need to call Value() on it to get the underlying interface{} value.

Changes:
- Get EntitlementValue from EntitlementField using GetValue()
- Call Value() method to extract the underlying value
- Properly type assert to string before parsing the expiration date

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@github-actions
Copy link

github-actions bot commented Oct 31, 2025

This PR has been released (on staging) and is available for download with a embedded-cluster-smoke-test-staging-app license ID.

Online Installer:

curl "https://staging.replicated.app/embedded/embedded-cluster-smoke-test-staging-app/ci/appver-dev-411e59b" -H "Authorization: $EC_SMOKE_TEST_LICENSE_ID" -o embedded-cluster-smoke-test-staging-app-ci.tgz

Airgap Installer (may take a few minutes before the airgap bundle is built):

curl "https://staging.replicated.app/embedded/embedded-cluster-smoke-test-staging-app/ci-airgap/appver-dev-411e59b?airgap=true" -H "Authorization: $EC_SMOKE_TEST_LICENSE_ID" -o embedded-cluster-smoke-test-staging-app-ci.tgz

Happy debugging!

This commit fixes test failures after merging main by:

1. Updating error message expectations in CLI tests to match the new
   LicenseWrapper error format ("failed to parse license file" instead
   of "failed to parse the license file")

2. Adding missing licenseID fields to all test license YAML fixtures
   in Test_verifyLicense, which are now required for the LicenseWrapper
   to recognize licenses as valid (licenses without IDs were being
   treated as missing licenses)

3. Removing malformed test cases from pkg/helpers/parse_test.go that
   had duplicate field definitions and wrong field types, which were
   preventing compilation

All affected tests now pass:
- Test_verifyLicense: All 13 test cases passing ✅
- Test_buildInstallConfig_License: All 6 test cases passing ✅
- pkg/helpers parsing tests: All test cases passing ✅

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
crdant and others added 5 commits November 7, 2025 13:30
Updated test files to properly use LicenseWrapper instead of raw License types:
- Added gopkg.in/yaml.v3 import to client_test.go
- Updated newTestLicense helper to return *licensewrapper.LicenseWrapper
- Wrapped License instances in LicenseWrapper structs in test setup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@crdant crdant requested a review from emosbaugh November 10, 2025 18:38
@crdant crdant force-pushed the feature/crdant/supports-license-v1beta2 branch 2 times, most recently from 8abf66b to 820b04b Compare November 12, 2025 04:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants