Skip to content

Conversation

@crdant
Copy link
Member

@crdant crdant commented Oct 23, 2025

What does this PR do?

TL;DR

This update modernizes the SDK to handle multiple license versions, preparing for evolving license formats while maintaining backward compatibility.

Details

Introduces a new license wrapper pattern that enables seamless SDK operation with different license schema versions. Instead of being locked to a single v1beta1 license format, the code now uses a wrapper type that accommodates both existing v1beta1 licenses and future v1beta2 licenses. This architectural change provides the flexibility needed for license schema evolution without breaking existing integrations.

The implementation preserves all existing functionality through accessor methods on the wrapper, ensuring code that interacts with licenses continues working unchanged. The wrapper intelligently routes operations to the appropriate underlying license version—whether verifying signatures with MD5 for v1beta1 or SHA-256 for v1beta2, or accessing entitlements in their native format without conversion overhead.

This positions the SDK to support an upcoming license enhancement while maintaining backward compatibility—a critical requirement for infrastructure tooling that must work reliably across diverse customer environments with varying license versions.

I feel weird about the next question…users can see this but it's also behind a feature flag so I think the answer is "NONE".

Does this PR introduce a user-facing change?

NONE

crdant and others added 4 commits October 23, 2025 14:01
Updates kotskinds from v0.0.0-20230724164735-f83482cc9cfe to
v0.0.0-20251023161058-b6489d3d51c5 to gain access to the new
v1beta2 License API. This version includes both v1beta1 and v1beta2
license types, allowing the SDK to support both API versions during
a gradual migration period.

This is the first step toward supporting the new v1beta2 License API
while maintaining backward compatibility with existing v1beta1 licenses.

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

Co-Authored-By: Claude <[email protected]>
Introduces a new LicenseWrapper type that can hold either a v1beta1 or
v1beta2 License object, providing a unified interface to access common
license fields regardless of the API version.

The wrapper includes:
- Version detection methods (IsV1, IsV2)
- Accessor methods for all common license fields (AppSlug, LicenseID,
  CustomerName, etc.)
- Support for all boolean feature flags (IsAirgapSupported,
  IsGitOpsSupported, etc.)

This abstraction allows the SDK to work with both license versions
transparently, eliminating the need for version checks throughout
the codebase. The wrapper always has exactly one field populated
(either V1 or V2), never both.

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

Co-Authored-By: Claude <[email protected]>
Modifies LoadLicenseFromBytes to detect and parse both v1beta1 and
v1beta2 license formats, returning a LicenseWrapper that encapsulates
the appropriate version. The loader now accepts both API versions and
creates the correct wrapper type based on the detected GVK.

Updates the Store interface and InMemoryStore implementation to use
LicenseWrapper instead of the concrete v1beta1.License type. This
enables the SDK to store and retrieve licenses of either version
transparently.

Key changes:
- LoadLicenseFromBytes returns LicenseWrapper instead of *v1beta1.License
- Store interface methods accept/return LicenseWrapper
- InMemoryStore properly deep copies the correct license version
- Uses wrapper accessor methods (GetAppSlug, GetLicenseType) instead
  of direct field access

This completes the core infrastructure needed to support both license
API versions throughout the SDK while maintaining a clean, version-
agnostic API surface.

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

Co-Authored-By: Claude <[email protected]>
Refactors signature verification to support both v1beta1 (MD5) and v1beta2 (SHA-256) licenses:
- Refactors VerifySignature() to accept LicenseWrapper and dispatch to version-specific verification
- Adds verifyV1Signature() for backward-compatible MD5 verification of v1beta1 licenses
- Adds verifyV2Signature() for SHA-256 verification of v1beta2 licenses
- Implements VerifySHA256() helper function using RSA-PSS with SHA-256 hashing

This enables the SDK to verify v1beta2 licenses using the more secure SHA-256 algorithm instead of MD5, while maintaining full backward compatibility for existing v1beta1 licenses.

[Phase 3 of v1beta2 license support]

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

Co-Authored-By: Claude <[email protected]>
crdant and others added 2 commits October 23, 2025 17:53
Updates all code to use the LicenseWrapper type instead of direct kotsv1beta1.License references. This completes the integration of the abstraction layer that supports both v1beta1 and v1beta2 license APIs.

Changes include:
- Update function signatures in pkg/license/, pkg/report/, pkg/handlers/, pkg/integration/, pkg/apiserver/, and pkg/upstream/ to accept LicenseWrapper
- Replace direct license.Spec.* accesses with wrapper getter methods
- Update imports to use licensetypes package
- Modify LicenseInfo struct to support both v1 and v2 entitlements natively
- Fix tests to construct and use LicenseWrapper pattern
- Update mock store to work with LicenseWrapper

This refactoring maintains backward compatibility with v1beta1 licenses while enabling future v1beta2 license support through the wrapper abstraction.

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

Co-Authored-By: Claude <[email protected]>
Refactors pact test files to use the LicenseWrapper type instead of raw
kotsv1beta1.License pointers, ensuring the contract tests match the new
LicenseWrapper-based API signatures introduced in the license abstraction layer.

Changes include:
- Wrapping v1beta1.License instances in LicenseWrapper{V1: ...} in test setup
- Updating mock store expectations to return LicenseWrapper types
- Adding licensetypes import to all affected pact test files

This maintains pact contract compatibility while supporting both v1beta1 and
v1beta2 license APIs through the abstraction layer.

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

Co-Authored-By: Claude <[email protected]>
@crdant crdant requested a review from diamonwiggins October 24, 2025 00:18
crdant and others added 2 commits October 24, 2025 14:10
Changes:
- Update InnerSignature struct to support dual-algorithm signatures
  (v1beta1 uses MD5, v1beta2 uses SHA-256)
- Change v1beta2.License field from Signature256 to Signature
- Update v1beta2 verification to use V2KeySignature and V2LicenseSignature
- Add complete field-by-field validation for v1beta2 licenses
- Add entitlement signature verification for both v1beta1 and v1beta2
- Add GetV2AppPublicKey helper function for v1beta2 entitlements
- Update kotskinds to latest version from main

The SDK now correctly unmarshals signatures from vandoor's
dual-algorithm format where both MD5 and SHA-256 signatures
are present in the same inner signature structure.

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

Co-Authored-By: Claude <[email protected]>
Vandoor signs entitlement values using fmt.Sprint(), not JSON marshaling.
Changed verification to use fmt.Sprint() to match vandoor's signing format.

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

Co-Authored-By: Claude <[email protected]>
@laverya
Copy link
Member

laverya commented Oct 27, 2025

I would recommend updating this to use the latest kotskinds version, which includes builtin validation functions for each license version

crdant and others added 4 commits October 27, 2025 16:04
Updates github.com/replicatedhq/kotskinds from v0.0.0-20251024162531-2174a5b85a4d to v0.0.0-20251024204505-044aa5d007d5 to get the ValidateLicense() methods for both v1beta1 and v1beta2 licenses.

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

Co-Authored-By: Claude <[email protected]>
Replaces custom crypto implementation with kotskinds' built-in ValidateLicense() method for both v1beta1 and v1beta2 licenses. This removes approximately 493 lines of custom RSA signature verification, license field validation, and entitlement signature checking code.

The ValidateLicense() method handles:
- MD5 signature verification for v1beta1 licenses
- SHA-256 signature verification for v1beta2 licenses
- License field integrity checks
- Entitlement signature validation
- Old and new signature format support

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

Co-Authored-By: Claude <[email protected]>
Updates chart version and appVersion from 1.0.0 to 1.10.0 to reflect license validation improvements.

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

Co-Authored-By: Claude <[email protected]>
@crdant
Copy link
Member Author

crdant commented Oct 28, 2025

I would recommend updating this to use the latest kotskinds version, which includes builtin validation functions for each license version

I think this was a timing issue. The PR was created before I incorporated the kotskinds validation.

crdant and others added 10 commits October 28, 2025 13:50
The LicenseFieldSignature struct was only capturing v1 (MD5) signatures,
causing v2 (SHA-256) signatures from v1beta2 licenses to be discarded
during JSON unmarshaling. This resulted in empty signature objects when
calling the /license/fields endpoint with v1beta2 licenses.

Updated LicenseFieldSignature to include both v1 and v2 fields to support
signature validation for both v1beta1 and v1beta2 license formats.

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

Co-Authored-By: Claude <[email protected]>
This commit completes Phase 2 of the LicenseWrapper consolidation by migrating
replicated-sdk to use the shared licensewrapper package from kotskinds instead
of maintaining its own duplicate implementation.

Changes:
- Updated go.mod to reference kotskinds with pkg/licensewrapper support
- Removed local pkg/license/types/license_wrapper.go (18 methods)
- Updated all imports from local licensetypes.LicenseWrapper to kotskinds licensewrapper.LicenseWrapper
- Simplified pkg/license/util.go to wrap kotskinds loader functions
- Regenerated mock store to reflect new types
- Updated 15 files across the codebase:
  - pkg/store (store_interface.go, memory_store.go, mock/mock_store.go)
  - pkg/license (license.go, signature.go, util.go)
  - pkg/report (instance.go, util.go)
  - pkg/integration, pkg/upstream, pkg/handlers, pkg/apiserver

All tests pass and signature validation works correctly using kotskinds ValidateLicense methods.

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

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

Updates kotskinds from b7dd48f3cb2b to 174e89c93554 (commit 174e89c9).

This brings in the merged PR #44 from kotskinds main branch which includes:
- EntitlementFieldWrapper type for version-agnostic entitlement access
- GetEntitlements() method returning wrapped entitlements
- Accessor methods: GetTitle(), GetDescription(), GetValue(), GetValueType(),
  IsHidden(), GetSignature()

This enables replicated-sdk to use EntitlementFieldWrapper to simplify
entitlement access and eliminate version-specific conditional logic.

Related: kotskinds PR #44

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

Co-Authored-By: Claude <[email protected]>
Replaces duplicated version-specific conditional logic with unified
GetEntitlements() access from kotskinds EntitlementFieldWrapper.

Changes:
- Replace if wrapper.V1/V2 != nil blocks with single GetEntitlements() call
- Use ent.GetValueType() instead of direct val.ValueType access
- Use ent.GetValue().StrVal instead of direct val.Value.StrVal access
- Eliminates 14 lines of duplicated code (36 lines -> 24 lines)

Benefits:
- Single code path for all license versions
- Version-agnostic entitlement access
- Forward-compatible with v1beta3+
- More maintainable and easier to understand

All existing tests pass without modification.

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

Co-Authored-By: Claude <[email protected]>
Updates HTTP handler to use GetEntitlements() internally while maintaining
backward-compatible API response structure.

Changes:
- Replace direct wrapper.V1.Spec.Entitlements access with GetEntitlements()
- Use wrapper.IsV1() and wrapper.IsV2() helper methods
- Add conversion loop to extract underlying EntitlementField structs
- Maintain identical API response structure (v1Entitlements/v2Entitlements)

Benefits:
- Uses unified GetEntitlements() API internally
- Maintains full backward compatibility
- API consumers see no change in response structure
- Cleaner version checking with IsV1()/IsV2() methods

All existing tests pass without modification.

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

Co-Authored-By: Claude <[email protected]>
Updates test files to import licensewrapper from kotskinds instead of
the deleted pkg/license/types package.

Changes:
- Replace licensetypes import with licensewrapper from kotskinds
- Update all licensetypes.LicenseWrapper references to licensewrapper.LicenseWrapper

Files updated:
- pact/license_test.go
- pact/instance_test.go
- pact/custom_metrics_test.go
- pkg/integration/integration_test.go
- pkg/report/util_test.go
- pkg/report/instance_test.go

All pkg/ tests pass successfully.

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

Co-Authored-By: Claude <[email protected]>
BREAKING CHANGE: LicenseInfo JSON response structure has changed

Previously, the LicenseInfo API response included version-specific
entitlements fields:
- v1Entitlements: map[string]kotsv1beta1.EntitlementField
- v2Entitlements: map[string]kotsv1beta2.EntitlementField

Now, there is a single unified entitlements field:
- entitlements: map[string]EntitlementFieldWrapper

The EntitlementFieldWrapper provides version-agnostic access to
entitlement data through accessor methods:
- GetTitle()
- GetDescription()
- GetValue()
- GetValueType()
- IsHidden()
- GetSignature()

Benefits:
- Eliminates version-specific conditional logic in API consumers
- Provides single, consistent entitlements structure
- Simplifies licenseInfoFromWrapper() implementation
- Removes dependency on kotsv1beta1 and kotsv1beta2 imports

All tests pass with this change.

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

Co-Authored-By: Claude <[email protected]>
The LicenseInfo API response now correctly serializes entitlements as
EntitlementField objects instead of EntitlementFieldWrapper objects.

Previously, the JSON would have looked like:
```json
{
  "entitlements": {
    "some_field": {
      "V1": { "title": "...", "value": "..." },
      "V2": null
    }
  }
}
```

Now it correctly serializes as:
```json
{
  "entitlements": {
    "some_field": {
      "title": "...",
      "value": "...",
      "valueType": "..."
    }
  }
}
```

Implementation:
- Convert EntitlementFieldWrapper map to EntitlementField map
- For V1 licenses: use EntitlementField directly
- For V2 licenses: convert v1beta2.EntitlementField to v1beta1.EntitlementField
  (they are structurally identical, only signature field differs)
- This maintains the unified entitlements API while providing proper JSON structure

All tests pass with this change.

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

Co-Authored-By: Claude <[email protected]>
BREAKING CHANGE (REVERTED): This fixes the previous commit that incorrectly
converted v1beta2 licenses to v1beta1 format in the API response.

The API now correctly returns:
- v1beta1.EntitlementField for v1beta1 licenses (with MD5 signature in v1 field)
- v1beta2.EntitlementField for v1beta2 licenses (with SHA-256 signature in v2 field)

This preserves the actual license format and signature data instead of
incorrectly converting v2 signatures to v1 format.

Implementation:
- Changed Entitlements field type from map[string]EntitlementField to interface{}
- Detect license version with wrapper.IsV1() / wrapper.IsV2()
- Return appropriate version-specific map
- Preserves signature format integrity

This is the correct approach - the API response should reflect the actual
license version rather than forcing everything into v1 format.

All tests pass with this change.

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

Co-Authored-By: Claude <[email protected]>
Comment on lines +112 to +135
func licenseInfoFromWrapper(wrapper licensewrapper.LicenseWrapper) LicenseInfo {
// Convert EntitlementFieldWrapper map to version-specific EntitlementField map
// Return v1 format for v1 licenses, v2 format for v2 licenses
var entitlements interface{}
wrappedEntitlements := wrapper.GetEntitlements()
if wrappedEntitlements != nil {
if wrapper.IsV1() {
v1Entitlements := make(map[string]kotsv1beta1.EntitlementField, len(wrappedEntitlements))
for key, wrapped := range wrappedEntitlements {
if wrapped.V1 != nil {
v1Entitlements[key] = *wrapped.V1
}
}
entitlements = v1Entitlements
} else if wrapper.IsV2() {
v2Entitlements := make(map[string]kotsv1beta2.EntitlementField, len(wrappedEntitlements))
for key, wrapped := range wrappedEntitlements {
if wrapped.V2 != nil {
v2Entitlements[key] = *wrapped.V2
}
}
entitlements = v2Entitlements
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love this but I'll accept it for now

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

me neither, but we have a static typed language and need a flexible JSON output

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, another layer or two down from this we have an entitlements kind with both V1 and V2 signatures
so we could just pass that up a few more layers and avoid a lot of confusion

@laverya laverya merged commit 686bd3d into main Oct 29, 2025
2 checks passed
@laverya laverya deleted the feature/crdant/adds-license-v1beta2-support branch October 29, 2025 16:10
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