How the JFrog Crossplane Providers Work

Learn the Crossplane building blocks, managed resources, the reconcile loop, and JFrog identifiers.

Learn how Crossplane and the JFrog Crossplane providers turn Kubernetes objects into JFrog objects. Read this page before you write your first manifests.

Building Blocks

A JFrog setup with Crossplane uses the following building blocks:

  • Crossplane core runs in crossplane-system. You install it with Helm. It installs packages and manages role-based access control (RBAC). The providers talk to JFrog.
  • Provider (pkg.crossplane.io/v1) downloads and installs a provider package.
    • Each version becomes a ProviderRevision.
    • The status columns INSTALLED and HEALTHY must both be True.
    • Key fields:
      • package: The Open Container Initiative (OCI) image.
      • packagePullSecrets: Required for releases-authenticated-docker.jfrog.io.
      • runtimeConfigRef: The DeploymentRuntimeConfig to use.
      • revisionActivationPolicy: Automatic or Manual.
      • packagePullPolicy.
  • DeploymentRuntimeConfig (pkg.crossplane.io/v1beta1) customizes the provider Deployment.
    • It sets imagePullSecrets, container args (provider flags), resources, env, volumes, and replicas.
    • The container must be named package-runtime.
    • deploymentTemplate.spec.selector: {} is required.
    • One configuration can serve all providers.
  • Managed resource: One Kubernetes object per JFrog object. The JFrog providers define 260 Kinds.
  • ClusterProviderConfig and ProviderConfig: The JFrog URL and credentials.
    • Each provider has its own configuration API group (artifactory.m.crossplane.io, xray.m.crossplane.io, project.m.crossplane.io, platform.m.crossplane.io, version v1beta1).
    • You need one configuration per provider, even if all of them use the same Secret.
    • The provider records a ProviderConfigUsage per managed resource.
    • You can't delete a configuration while managed resources use it.

Provider Architecture

Each provider is generated with Upjet from the official JFrog Terraform provider. It runs that provider's code in-process. There is no Terraform CLI, no workspace, and no state file on disk.

Validation, defaults, and API behavior of the upstream resources apply unchanged. The upstream JFrog Terraform documentation for a resource is therefore a valid source of field-level detail.

The following table lists each provider, its upstream Terraform provider, and its Kinds.

ProviderUpstreamKindsAPI groups (<group>.<provider>.m.crossplane.io/v1alpha1)
provider-artifactoryjfrog/artifactory 12.11.14214repository (150), webhook (24), configuration (16), security (11), replication (5), user (4), lifecycle (2), artifact (2)
provider-xrayjfrog/xray 3.1.1321policy (7), configuration (7), report (5), watch (1), webhook (1)
provider-projectjfrog/project 1.9.98access (3), repository (3), project (2)
provider-platformjfrog/platform 2.2.1117access (6), authentication (6), configuration (3), lifecycle (2)

Repository Kinds are named <Class><PackageType>Repository, for example LocalDockerV2Repository or RemoteNPMRepository. Webhook Kinds are named <Domain>Webhook and <Domain>CustomWebhook.

Anatomy of a Managed Resource

The following manifest shows the main parts of a managed resource:

apiVersion: repository.artifactory.m.crossplane.io/v1alpha1
kind: LocalDockerV2Repository
metadata:
  name: team-docker-local            # for most Kinds: the JFrog identifier (here the repository key)
  namespace: team-a
  annotations:
    crossplane.io/external-name: team-docker-local   # set automatically when omitted
spec:
  providerConfigRef: {kind: ClusterProviderConfig, name: jfrog}   # both fields required when written
  managementPolicies: ["*"]          # default
  forProvider:                       # desired state, enforced continuously
    description: Team images
    xrayIndex: true
  initProvider: {}                   # values applied only at creation
status:
  atProvider: {}                     # observed state
  conditions: []                     # Synced, Ready, LastAsyncOperation

If you omit providerConfigRef entirely, it defaults to {kind: ClusterProviderConfig, name: default}.

Namespaced and Legacy Managed Resources

Each Kind exists in a namespaced and a legacy cluster-scoped variant. They differ as follows.

Namespaced (*.m.crossplane.io, recommended)Legacy cluster-scoped (*.crossplane.io)
ScopeNamespace (team isolation through RBAC)Cluster
providerConfigRef{kind: ClusterProviderConfig or ProviderConfig, name}{name} of a legacy ProviderConfig
Keep the JFrog object on deletemanagementPolicies without DeletedeletionPolicy: Orphan
Secret references{name, key} in the managed resource's namespace{name, namespace, key}

The Reconcile Loop

For every managed resource, the provider repeats these steps:

  1. Skips the object if it is paused (crossplane.io/paused: "true" or managementPolicies: []).
  2. Sets the crossplane.io/external-name annotation (defaults to metadata.name) and adds the finalizer finalizer.managedresource.crossplane.io.
  3. Resolves references (...Ref, ...Selector).
  4. Connects using the ProviderConfig and credentials.
  5. Observes the JFrog object, then late-initializes it. Late initialization copies server defaults of optional fields you didn't set into spec.forProvider.
  6. Creates the object if it is missing, updates it if it differs, or deletes it if the managed resource is being deleted.
  7. Requeues the object. Healthy objects are checked again every poll interval (--poll, default 10m in these providers). Failures retry with exponential backoff from 1 s to 60 s. You can't configure the backoff.

Create, update, and delete run asynchronously. Their result appears in the LastAsyncOperation condition.

Any change to the managed resource triggers an immediate reconcile. To force a reconcile, run the following command:

kubectl annotate <fq-kind>/<name> -n <ns> reconcile/kick="$(date +%s)" --overwrite

If the provider restarts in the middle of a create, Crossplane refuses to create the object again, to avoid duplicates. It reports cannot determine creation result - remove the crossplane.io/external-create-pending annotation if it is safe to proceed. For more information about the fix, see Troubleshoot the JFrog Crossplane Providers.

Conditions

Each managed resource reports the following conditions.

ConditionMeaning
Synced=True (ReconcileSuccess)The last reconcile succeeded.
Synced=False (ReconcileError)The last reconcile failed. The message has the error (references, credentials, validation, API).
Synced=False (ReconcilePaused)The object is paused.
Ready=True (Available)The JFrog object exists.
Ready=False (Creating, Deleting, Unavailable)The object isn't yet created, is being deleted, or is unhealthy.
LastAsyncOperation=False (AsyncCreateFailure, AsyncUpdateFailure, AsyncDeleteFailure, ApplyFailure, DestroyFailure)The JFrog API request failed. The message contains the API response.

SYNCED=False for a short time after you apply several objects together is normal. Objects wait for their dependencies and converge through retries.

kubectl describe shows the events of a managed resource. The events include the following:

  • CreatedExternalResource, UpdatedExternalResource, and DeletedExternalResource.
  • Failure events: CannotResolveResourceReferences, CannotConnectToProvider, CannotCreateExternalResource, CannotUpdateExternalResource, and CannotDeleteExternalResource.

forProvider, initProvider, and Late Initialization

The provider treats these parts of a managed resource as follows:

  • The provider enforces spec.forProvider on every poll and reverts manual changes in JFrog.
  • spec.initProvider has the same shape, but the provider sends it only at creation. After that, the provider ignores it for drift.
    • Use it for values that someone else owns later. For example, set a user's initial groups here when membership is managed elsewhere.
    • You can supply required fields in either forProvider or initProvider.
  • Late initialization writes server defaults into spec.forProvider. After that, the provider enforces them too. To enforce only what you wrote, omit LateInitialize from managementPolicies.
  • status.atProvider shows the observed state, including read-only fields such as id. Secret values are never shown.

Some fields can take their value from another managed resource. For more information, see Reference Other Managed Resources.

Identifiers and External Names

The crossplane.io/external-name annotation stores the JFrog identifier. Each Kind uses one of the following strategies. The reference entry for each Kind states its strategy. For more information, see JFrog Crossplane Kinds Reference.

StrategyHow it worksKinds
Name as identifiermetadata.name (or the annotation) is the JFrog key or name. The identifying field isn't in spec.Most Kinds: repositories, webhooks, users, groups, policies, watches, projects, roles, permissions
Spec fieldThe identifier is a normal spec field. metadata.name is free.Platform GroupMembers, SCIMUser, AWSIAMRole, WorkersService. Xray RepositoryConfig. Project ShareRepositoryWithAll.
CompositeBuilt from several fieldsproject ShareRepository (<repoKey>:<targetProjectKey>), ReleaseBundleV2 (<name>:<version>), ReleaseBundleV2Promotion (<name>:<version>:<environment>), ItemProperties
Server-assigned IDJFrog returns an ID that the provider writes into the annotation. Never edit or remove it, or a duplicate is created.Xray CurationPolicy, CustomCurationCondition, IgnoreRule, reports. Artifactory replications, OauthSettings, SamlSettings, GeneralSecurity, AnonymousUser, APIKey, ScopedToken, Artifact
SingletonConstant identifier, one per JFrog PlatformMailServer, TrashcanConfig, xray Settings, WorkersCount, CatalogLabels, platform CrowdSettings, HTTPSSOSettings, License, ReverseProxy, Lifecycle (per project or global)

The following rules apply to identifiers:

  • When the JFrog identifier isn't a valid Kubernetes name (uppercase, underscores), use any valid metadata.name. Then set the annotation before the first apply.
  • Renaming a managed resource targets a different JFrog object. It doesn't rename the JFrog object.
  • A managed resource whose identifier matches an existing object adopts that object instead of creating a duplicate. For the adoption procedure, see Adopt Existing JFrog Objects.

Next Steps


Did this page help you?