Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/OpenShiftPipelinesAsCode.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,33 @@ them as per their need.

Details of the field can be found in [OpenShift Pipelines As Code Settings][pac-config]

#### Trusted Provider Hostnames

`trusted-provider-hostnames` is an administrator-owned allowlist of git provider hostnames to which the controller may
send provider credentials. It can be set in `settings` of the `OpenShiftPipelinesAsCode` CR, or through `TektonConfig`
(`spec.platforms.openshift.pipelinesAsCode.settings`), and for every entry of `additionalPACControllers`.

```yaml
spec:
platforms:
openshift:
pipelinesAsCode:
settings:
trusted-provider-hostnames: "ghe.example.com,gitlab.example.com"
```

When the list is empty, known public provider hosts remain trusted and each controller may learn a publicly routable
self-hosted hostname from an authenticated GitHub App webhook. Learned hosts are stored by the controller in the
`pipelinesascode.tekton.dev/auto-trusted-provider-hostnames` annotation of its own ConfigMap; the operator preserves this
annotation. When the list is not empty it is authoritative and no host is learned.

Every enabled additional controller gets a Role and RoleBinding allowing the `pipelines-as-code-controller` ServiceAccount to
`get`, `update` and `patch` only that controller's ConfigMap (not created when the controller uses the `pipelines-as-code` ConfigMap).

> **Upgrade note:** self-hosted providers using per-repository or incoming webhooks cannot teach the controller a hostname,
> so configure `trusted-provider-hostnames` before upgrading. GitHub.com installations need no additional configuration.
> This requires a Pipelines as Code release that includes `trusted-provider-hostnames`.

#### Remote Hub Catalogs

Pipelines as Code supports configuring remote hub catalogs to fetch tasks and pipelines. You can configure custom catalogs using the `catalog-{INDEX}-*` settings pattern.
Expand Down
51 changes: 51 additions & 0 deletions pkg/reconciler/openshift/openshiftpipelinesascode/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,15 @@ import (
"go.uber.org/zap"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)

const (
pipelinesAsCodeCM = "pipelines-as-code"
pacControllerServiceAccount = "pipelines-as-code-controller"
additionalPACControllerNameSuffix = "-pac-controller"
)

Expand Down Expand Up @@ -86,6 +89,16 @@ func additionalControllerTransform(extension common.Extension, name string) clie
pac := comp.(*v1alpha1.OpenShiftPipelinesAsCode)
additionalPACControllerConfig := pac.Spec.PACSettings.AdditionalPACControllers[name]

// give the additional controller access to its own configmap, so that it can
// record the learned trusted provider hostnames in the configmap annotation
if additionalPACControllerConfig.ConfigMapName != pipelinesAsCodeCM {
rbacManifest, err := additionalControllerConfigMapRBAC(name, additionalPACControllerConfig.ConfigMapName, pac.Spec.GetTargetNamespace())
if err != nil {
return &mf.Manifest{}, err
}
*manifest = manifest.Append(rbacManifest)
}

imagesRaw := common.ToLowerCaseKeys(common.ImagesFromEnv(common.PacImagePrefix))
images := common.ImageRegistryDomainOverride(imagesRaw)
// Run transformers
Expand Down Expand Up @@ -120,6 +133,44 @@ func additionalControllerTransform(extension common.Extension, name string) clie
}
}

// additionalControllerConfigMapRBAC returns a Role and RoleBinding which allow the
// additional PACController to get, update and patch only its own configMap
func additionalControllerConfigMapRBAC(name, configMapName, namespace string) (mf.Manifest, error) {
rbacName := fmt.Sprintf("%s%s-configmap", name, additionalPACControllerNameSuffix)
labels := map[string]string{"app.kubernetes.io/part-of": "pipelines-as-code"}

role := &rbacv1.Role{
TypeMeta: metav1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "Role"},
ObjectMeta: metav1.ObjectMeta{Name: rbacName, Namespace: namespace, Labels: labels},
Rules: []rbacv1.PolicyRule{{
APIGroups: []string{""},
Resources: []string{"configmaps"},
ResourceNames: []string{configMapName},
Verbs: []string{"get", "update", "patch"},
}},
}
roleBinding := &rbacv1.RoleBinding{
TypeMeta: metav1.TypeMeta{APIVersion: "rbac.authorization.k8s.io/v1", Kind: "RoleBinding"},
ObjectMeta: metav1.ObjectMeta{Name: rbacName, Namespace: namespace, Labels: labels},
RoleRef: rbacv1.RoleRef{APIGroup: "rbac.authorization.k8s.io", Kind: "Role", Name: rbacName},
Subjects: []rbacv1.Subject{{
Kind: rbacv1.ServiceAccountKind,
Name: pacControllerServiceAccount,
Namespace: namespace,
}},
}

resources := []unstructured.Unstructured{}
for _, obj := range []interface{}{role, roleBinding} {
content, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return mf.Manifest{}, err
}
resources = append(resources, unstructured.Unstructured{Object: content})
}
return mf.ManifestFrom(mf.Slice(resources))
}

// This returns all resources to deploy for the additional PACController
func filterAdditionalControllerManifest(manifest mf.Manifest) mf.Manifest {
// filter deployment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"gotest.tools/v3/assert"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/runtime"
)

Expand Down Expand Up @@ -188,3 +189,52 @@ func TestUpdateAdditionControllerConfigMap(t *testing.T) {
t.Errorf("failed to update additional pac controller route %s", diff.PrintWantGot(d))
}
}

func TestAdditionalControllerConfigMapRBAC(t *testing.T) {
m, err := additionalControllerConfigMapRBAC("test", "test-config", "pac-ns")
assert.NilError(t, err)
assert.Equal(t, len(m.Resources()), 2)

roles := m.Filter(mf.ByKind("Role")).Resources()
assert.Equal(t, len(roles), 1)
role := &rbacv1.Role{}
assert.NilError(t, runtime.DefaultUnstructuredConverter.FromUnstructured(roles[0].Object, role))
assert.Equal(t, role.Name, "test-pac-controller-configmap")
assert.Equal(t, role.Namespace, "pac-ns")
assert.DeepEqual(t, role.Rules, []rbacv1.PolicyRule{{
APIGroups: []string{""},
Resources: []string{"configmaps"},
ResourceNames: []string{"test-config"},
Verbs: []string{"get", "update", "patch"},
}})

bindings := m.Filter(mf.ByKind("RoleBinding")).Resources()
assert.Equal(t, len(bindings), 1)
rb := &rbacv1.RoleBinding{}
assert.NilError(t, runtime.DefaultUnstructuredConverter.FromUnstructured(bindings[0].Object, rb))
assert.Equal(t, rb.RoleRef.Name, role.Name)
assert.DeepEqual(t, rb.Subjects, []rbacv1.Subject{{
Kind: "ServiceAccount",
Name: "pipelines-as-code-controller",
Namespace: "pac-ns",
}})
}

func TestUpdateAdditionControllerConfigMapPreservesAnnotations(t *testing.T) {
const learnedAnnotation = "pipelinesascode.tekton.dev/auto-trusted-provider-hostnames"
testData := path.Join("testdata", "test-additional-pac-cm.yaml")
manifest, err := mf.ManifestFrom(mf.Recursive(testData))
assert.NilError(t, err)

resources := manifest.Resources()
resources[0].SetAnnotations(map[string]string{learnedAnnotation: "ghe.example.com"})
manifest, err = mf.ManifestFrom(mf.Slice(resources))
assert.NilError(t, err)

updated, err := manifest.Transform(updateAdditionControllerConfigMap(v1alpha1.AdditionalPACControllerConfig{
ConfigMapName: "test-config",
Settings: map[string]string{"application-name": "test"},
}))
assert.NilError(t, err)
assert.Equal(t, updated.Resources()[0].GetAnnotations()[learnedAnnotation], "ghe.example.com")
}