Skip to content

Commit 4fec4af

Browse files
authored
Custom improvements for OpenShift (#825)
* Custom improvements for OpenShift Signed-off-by: Matej Vasek <[email protected]> * fixup: osh cred loader Signed-off-by: Matej Vasek <[email protected]> * fixup: style Signed-off-by: Matej Vasek <[email protected]> * fixup: dns-err detec for fallback in cluster dial Signed-off-by: Matej Vasek <[email protected]>
1 parent fba0189 commit 4fec4af

File tree

7 files changed

+249
-13
lines changed

7 files changed

+249
-13
lines changed

Diff for: cmd/build.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ kn func build --builder cnbs/sample-builder:bionic
5353
cmd.Flags().StringP("builder", "b", "", "Buildpack builder, either an as a an image name or a mapping name.\nSpecified value is stored in func.yaml for subsequent builds.")
5454
cmd.Flags().BoolP("confirm", "c", false, "Prompt to confirm all configuration options (Env: $FUNC_CONFIRM)")
5555
cmd.Flags().StringP("image", "i", "", "Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry (Env: $FUNC_IMAGE)")
56-
cmd.Flags().StringP("registry", "r", "", "Registry + namespace part of the image to build, ex 'quay.io/myuser'. The full image name is automatically determined based on the local directory name. If not provided the registry will be taken from func.yaml (Env: $FUNC_REGISTRY)")
56+
cmd.Flags().StringP("registry", "r", GetDefaultRegistry(), "Registry + namespace part of the image to build, ex 'quay.io/myuser'. The full image name is automatically determined based on the local directory name. If not provided the registry will be taken from func.yaml (Env: $FUNC_REGISTRY)")
5757
cmd.Flags().BoolP("push", "u", false, "Attempt to push the function image after being successfully built")
5858
setPathFlag(cmd)
5959

Diff for: cmd/client.go

+45-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package cmd
22

33
import (
4-
"net/http"
5-
64
fn "knative.dev/kn-plugin-func"
75
"knative.dev/kn-plugin-func/buildpacks"
86
"knative.dev/kn-plugin-func/docker"
97
"knative.dev/kn-plugin-func/docker/creds"
8+
fnhttp "knative.dev/kn-plugin-func/http"
109
"knative.dev/kn-plugin-func/knative"
10+
"knative.dev/kn-plugin-func/openshift"
1111
"knative.dev/kn-plugin-func/pipelines/tekton"
1212
"knative.dev/kn-plugin-func/progress"
1313
)
@@ -22,8 +22,36 @@ type ClientOptions struct {
2222

2323
type ClientFactory func(opts ClientOptions) *fn.Client
2424

25-
func NewClientFactory(transport http.RoundTripper) ClientFactory {
26-
return func(clientOptions ClientOptions) *fn.Client {
25+
// NewDefaultClientFactory returns a function that creates instances of function.Client and a cleanup routine.
26+
//
27+
// This function may allocate resources that are used by produced instances of function.Client.
28+
//
29+
// To free these resources (after instances of function.Client are no longer in use)
30+
// caller of this function has to invoke the cleanup routine.
31+
//
32+
// Usage:
33+
// newClient, cleanUp := NewDefaultClientFactory()
34+
// defer cleanUp()
35+
// fnClient := newClient()
36+
// // use your fnClient here...
37+
func NewDefaultClientFactory() (newClient ClientFactory, cleanUp func() error) {
38+
39+
var transportOpts []fnhttp.Option
40+
var additionalCredLoaders []creds.CredentialsCallback
41+
42+
switch {
43+
case openshift.IsOpenShift():
44+
transportOpts = append(transportOpts, openshift.WithOpenShiftServiceCA())
45+
additionalCredLoaders = openshift.GetDockerCredentialLoaders()
46+
default:
47+
}
48+
49+
transport := fnhttp.NewRoundTripper(transportOpts...)
50+
cleanUp = func() error {
51+
return transport.Close()
52+
}
53+
54+
newClient = func(clientOptions ClientOptions) *fn.Client {
2755
builder := buildpacks.NewBuilder()
2856
builder.Verbose = clientOptions.Verbose
2957

@@ -33,7 +61,8 @@ func NewClientFactory(transport http.RoundTripper) ClientFactory {
3361
credentialsProvider := creds.NewCredentialsProvider(
3462
creds.WithPromptForCredentials(newPromptForCredentials()),
3563
creds.WithPromptForCredentialStore(newPromptForCredentialStore()),
36-
creds.WithTransport(transport))
64+
creds.WithTransport(transport),
65+
creds.WithAdditionalCredentialLoaders(additionalCredLoaders...))
3766

3867
pusher := docker.NewPusher(
3968
docker.WithCredentialsProvider(credentialsProvider),
@@ -84,4 +113,15 @@ func NewClientFactory(transport http.RoundTripper) ClientFactory {
84113

85114
return fn.New(opts...)
86115
}
116+
117+
return newClient, cleanUp
118+
}
119+
120+
func GetDefaultRegistry() string {
121+
switch {
122+
case openshift.IsOpenShift():
123+
return openshift.GetDefaultRegistry()
124+
default:
125+
return ""
126+
}
87127
}

Diff for: cmd/deploy.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ kn func deploy --image quay.io/myuser/myfunc -n myns
5959
"You may provide this flag multiple times for setting multiple environment variables. "+
6060
"To unset, specify the environment variable name followed by a \"-\" (e.g., NAME-).")
6161
cmd.Flags().StringP("image", "i", "", "Full image name in the form [registry]/[namespace]/[name]:[tag] (optional). This option takes precedence over --registry (Env: $FUNC_IMAGE)")
62-
cmd.Flags().StringP("registry", "r", "", "Registry + namespace part of the image to build, ex 'quay.io/myuser'. The full image name is automatically determined based on the local directory name. If not provided the registry will be taken from func.yaml (Env: $FUNC_REGISTRY)")
62+
cmd.Flags().StringP("registry", "r", GetDefaultRegistry(), "Registry + namespace part of the image to build, ex 'quay.io/myuser'. The full image name is automatically determined based on the local directory name. If not provided the registry will be taken from func.yaml (Env: $FUNC_REGISTRY)")
6363
cmd.Flags().StringP("build", "b", fn.DefaultBuildType, fmt.Sprintf("Build specifies the way the function should be built. Supported types are %s (Env: $FUNC_BUILD)", fn.SupportedBuildTypes(true)))
6464
cmd.Flags().BoolP("push", "u", true, "Attempt to push the function image to registry before deploying (Env: $FUNC_PUSH)")
6565
setPathFlag(cmd)

Diff for: cmd/root.go

+4-5
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"knative.dev/client/pkg/util"
1515

1616
fn "knative.dev/kn-plugin-func"
17-
fnhttp "knative.dev/kn-plugin-func/http"
1817
)
1918

2019
var exampleTemplate = template.Must(template.New("example").Parse(`
@@ -90,11 +89,11 @@ Create, build and deploy Functions in serverless containers for multiple runtime
9089
newClient := config.NewClient
9190

9291
if newClient == nil {
93-
transport := fnhttp.NewRoundTripper()
94-
root.PostRun = func(cmd *cobra.Command, args []string) {
95-
transport.Close()
92+
var cleanUp func() error
93+
newClient, cleanUp = NewDefaultClientFactory()
94+
root.PersistentPostRunE = func(cmd *cobra.Command, args []string) error {
95+
return cleanUp()
9696
}
97-
newClient = NewClientFactory(transport)
9897
}
9998

10099
root.AddCommand(NewVersionCmd(version))

Diff for: http/transport.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ func (d *dialerWithFallback) DialContext(ctx context.Context, network, address s
119119
}
120120

121121
var dnsErr *net.DNSError
122-
if !(errors.As(err, &dnsErr) && dnsErr.IsNotFound) {
122+
if !errors.As(err, &dnsErr) {
123123
return nil, err
124124
}
125125

Diff for: openshift/openshift.go

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package openshift
2+
3+
import (
4+
"context"
5+
"crypto/x509"
6+
"encoding/pem"
7+
"errors"
8+
"strings"
9+
"sync"
10+
"time"
11+
12+
v1 "k8s.io/api/core/v1"
13+
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
"k8s.io/apimachinery/pkg/fields"
16+
"k8s.io/apimachinery/pkg/util/rand"
17+
18+
"knative.dev/kn-plugin-func/docker"
19+
"knative.dev/kn-plugin-func/docker/creds"
20+
fnhttp "knative.dev/kn-plugin-func/http"
21+
"knative.dev/kn-plugin-func/k8s"
22+
)
23+
24+
const (
25+
registryHost = "image-registry.openshift-image-registry.svc"
26+
registryHostPort = registryHost + ":5000"
27+
)
28+
29+
func GetServiceCA(ctx context.Context) (*x509.Certificate, error) {
30+
client, ns, err := k8s.NewClientAndResolvedNamespace("")
31+
if err != nil {
32+
return nil, err
33+
}
34+
35+
cfgMapName := "service-ca-config-" + rand.String(5)
36+
37+
cfgMap := &v1.ConfigMap{
38+
ObjectMeta: metav1.ObjectMeta{
39+
Name: cfgMapName,
40+
Annotations: map[string]string{"service.beta.openshift.io/inject-cabundle": "true"},
41+
},
42+
}
43+
44+
configMaps := client.CoreV1().ConfigMaps(ns)
45+
46+
nameSelector := fields.OneTermEqualSelector("metadata.name", cfgMapName).String()
47+
listOpts := metav1.ListOptions{
48+
Watch: true,
49+
FieldSelector: nameSelector,
50+
}
51+
52+
watch, err := configMaps.Watch(ctx, listOpts)
53+
if err != nil {
54+
return nil, err
55+
}
56+
defer watch.Stop()
57+
58+
crtChan := make(chan string)
59+
go func() {
60+
for event := range watch.ResultChan() {
61+
cm, ok := event.Object.(*v1.ConfigMap)
62+
if !ok {
63+
continue
64+
}
65+
if crt, ok := cm.Data["service-ca.crt"]; ok {
66+
crtChan <- crt
67+
close(crtChan)
68+
break
69+
}
70+
}
71+
}()
72+
73+
_, err = configMaps.Create(ctx, cfgMap, metav1.CreateOptions{})
74+
if err != nil {
75+
return nil, err
76+
}
77+
defer func() {
78+
_ = configMaps.Delete(ctx, cfgMapName, metav1.DeleteOptions{})
79+
}()
80+
81+
select {
82+
case crt := <-crtChan:
83+
blk, _ := pem.Decode([]byte(crt))
84+
return x509.ParseCertificate(blk.Bytes)
85+
case <-time.After(time.Second * 5):
86+
return nil, errors.New("failed to get OpenShift's service CA in time")
87+
}
88+
}
89+
90+
// WithOpenShiftServiceCA enables trust to OpenShift's service CA for internal image registry
91+
func WithOpenShiftServiceCA() fnhttp.Option {
92+
var selectCA func(ctx context.Context, serverName string) (*x509.Certificate, error)
93+
ca, err := GetServiceCA(context.TODO())
94+
if err == nil {
95+
selectCA = func(ctx context.Context, serverName string) (*x509.Certificate, error) {
96+
if strings.HasPrefix(serverName, registryHost) {
97+
return ca, nil
98+
}
99+
return nil, nil
100+
}
101+
}
102+
return fnhttp.WithSelectCA(selectCA)
103+
}
104+
105+
func GetDefaultRegistry() string {
106+
ns, _ := k8s.GetNamespace("")
107+
if ns == "" {
108+
ns = "default"
109+
}
110+
111+
return registryHostPort + "/" + ns
112+
}
113+
114+
func GetDockerCredentialLoaders() []creds.CredentialsCallback {
115+
conf := k8s.GetClientConfig()
116+
117+
rawConf, err := conf.RawConfig()
118+
if err != nil {
119+
return nil
120+
}
121+
122+
cc := rawConf.Contexts[rawConf.CurrentContext]
123+
authInfo := rawConf.AuthInfos[cc.AuthInfo]
124+
125+
var user string
126+
parts := strings.SplitN(cc.AuthInfo, "/", 2)
127+
if len(parts) >= 1 {
128+
user = parts[0]
129+
} else {
130+
return nil
131+
}
132+
credentials := docker.Credentials{
133+
Username: user,
134+
Password: authInfo.Token,
135+
}
136+
137+
return []creds.CredentialsCallback{
138+
func(registry string) (docker.Credentials, error) {
139+
if registry == registryHostPort {
140+
return credentials, nil
141+
}
142+
return docker.Credentials{}, nil
143+
},
144+
}
145+
146+
}
147+
148+
var isOpenShift bool
149+
var checkOpenShiftOnce sync.Once
150+
151+
func IsOpenShift() bool {
152+
checkOpenShiftOnce.Do(func() {
153+
client, err := k8s.NewKubernetesClientset()
154+
if err != nil {
155+
isOpenShift = false
156+
return
157+
}
158+
_, err = client.CoreV1().Services("openshift-image-registry").Get(context.TODO(), "image-registry", metav1.GetOptions{})
159+
if k8sErrors.IsNotFound(err) {
160+
isOpenShift = false
161+
return
162+
}
163+
isOpenShift = true
164+
})
165+
return isOpenShift
166+
}

Diff for: openshift/openshift_test.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package openshift_test
2+
3+
import (
4+
"net/http"
5+
6+
"testing"
7+
8+
fnhttp "knative.dev/kn-plugin-func/http"
9+
"knative.dev/kn-plugin-func/openshift"
10+
)
11+
12+
func TestRoundTripper(t *testing.T) {
13+
if !openshift.IsOpenShift() {
14+
t.Skip("The cluster in not an instance of OpenShift.")
15+
return
16+
}
17+
18+
transport := fnhttp.NewRoundTripper(openshift.WithOpenShiftServiceCA())
19+
defer transport.Close()
20+
21+
client := http.Client{
22+
Transport: transport,
23+
}
24+
25+
resp, err := client.Get("https://image-registry.openshift-image-registry.svc.cluster.local:5000/v2/")
26+
if err != nil {
27+
t.Error(err)
28+
return
29+
}
30+
defer resp.Body.Close()
31+
}

0 commit comments

Comments
 (0)