<commit_before>/*
Copyright 2019 The Jetstack cert-manager contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package ca

import (
	"bytes"
	"context"
	"crypto"
	"crypto/rand"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/asn1"
	"encoding/pem"
	"errors"
	"testing"
	"time"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	clientcorev1 "k8s.io/client-go/listers/core/v1"
	coretesting "k8s.io/client-go/testing"
	fakeclock "k8s.io/utils/clock/testing"

	apiutil "github.com/jetstack/cert-manager/pkg/api/util"
	"github.com/jetstack/cert-manager/pkg/apis/certmanager"
	cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2"
	cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
	"github.com/jetstack/cert-manager/pkg/controller/certificaterequests"
	testpkg "github.com/jetstack/cert-manager/pkg/controller/test"
	"github.com/jetstack/cert-manager/pkg/util/pki"
	"github.com/jetstack/cert-manager/test/unit/gen"
	testlisters "github.com/jetstack/cert-manager/test/unit/listers"
)

var (
	fixedClockStart = time.Now()
	fixedClock      = fakeclock.NewFakeClock(fixedClockStart)
)

func generateCSR(t *testing.T, secretKey crypto.Signer) []byte {
	asn1Subj, _ := asn1.Marshal(pkix.Name{
		CommonName: "test",
	}.ToRDNSequence())
	template := x509.CertificateRequest{
		RawSubject:         asn1Subj,
		SignatureAlgorithm: x509.SHA256WithRSA,
	}

	csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, secretKey)
	if err != nil {
		t.Error(err)
		t.FailNow()
	}

	csr := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes})

	return csr
}

func generateSelfSignedCertFromCR(t *testing.T, cr *cmapi.CertificateRequest, key crypto.Signer,
	duration time.Duration) (*x509.Certificate, []byte) {
	template, err := pki.GenerateTemplateFromCertificateRequest(cr)
	if err != nil {
		t.Errorf("error generating template: %v", err)
	}

	derBytes, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
	if err != nil {
		t.Errorf("error signing cert: %v", err)
		t.FailNow()
	}

	pemByteBuffer := bytes.NewBuffer([]byte{})
	err = pem.Encode(pemByteBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
	if err != nil {
		t.Errorf("failed to encode cert: %v", err)
		t.FailNow()
	}

	return template, pemByteBuffer.Bytes()
}

func TestSign(t *testing.T) {
	baseIssuer := gen.Issuer("test-issuer",
		gen.SetIssuerCA(cmapi.CAIssuer{SecretName: "root-ca-secret"}),
		gen.AddIssuerCondition(cmapi.IssuerCondition{
			Type:   cmapi.IssuerConditionReady,
			Status: cmmeta.ConditionTrue,
		}),
	)

	// Build root RSA CA
	skRSA, err := pki.GenerateRSAPrivateKey(2048)
	if err != nil {
		t.Error(err)
		t.FailNow()
	}

	skRSAPEM := pki.EncodePKCS1PrivateKey(skRSA)
	rsaCSR := generateCSR(t, skRSA)

	baseCR := gen.CertificateRequest("test-cr",
		gen.SetCertificateRequestIsCA(true),
		gen.SetCertificateRequestCSR(rsaCSR),
		gen.SetCertificateRequestIssuer(cmmeta.ObjectReference{
			Name:  baseIssuer.DeepCopy().Name,
			Group: certmanager.GroupName,
			Kind:  "Issuer",
		}),
		gen.SetCertificateRequestDuration(&metav1.Duration{Duration: time.Hour * 24 * 60}),
	)

	// generate a self signed root ca valid for 60d
	_, rsaPEMCert := generateSelfSignedCertFromCR(t, baseCR, skRSA, time.Hour*24*60)
	rsaCASecret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "root-ca-secret",
			Namespace: gen.DefaultTestNamespace,
		},
		Data: map[string][]byte{
			corev1.TLSPrivateKeyKey: skRSAPEM,
			corev1.TLSCertKey:       rsaPEMCert,
		},
	}

	badDataSecret := rsaCASecret.DeepCopy()
	badDataSecret.Data[corev1.TLSPrivateKeyKey] = []byte("bad key")

	template, err := pki.GenerateTemplateFromCertificateRequest(baseCR)
	if err != nil {
		t.Error(err)
		t.FailNow()
	}
	certPEM, _, err := pki.SignCSRTemplate([]*x509.Certificate{template}, skRSA, template)
	if err != nil {
		t.Error(err)
		t.FailNow()
	}

	metaFixedClockStart := metav1.NewTime(fixedClockStart)
	tests := map[string]testT{
		"a missing CA key pair should set the condition to pending and wait for a re-sync": {
			certificateRequest: baseCR.DeepCopy(),
			builder: &testpkg.Builder{
				KubeObjects:        []runtime.Object{},
				CertManagerObjects: []runtime.Object{baseCR.DeepCopy(), baseIssuer.DeepCopy()},
				ExpectedEvents: []string{
					`Normal SecretMissing Referenced secret default-unit-test-ns/root-ca-secret not found: secret "root-ca-secret" not found`,
				},
				ExpectedActions: []testpkg.Action{
					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
						cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
						"status",
						gen.DefaultTestNamespace,
						gen.CertificateRequestFrom(baseCR.DeepCopy(),
							gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
								Type:               cmapi.CertificateRequestConditionReady,
								Status:             cmmeta.ConditionFalse,
								Reason:             cmapi.CertificateRequestReasonPending,
								Message:            `Referenced secret default-unit-test-ns/root-ca-secret not found: secret "root-ca-secret" not found`,
								LastTransitionTime: &metaFixedClockStart,
							}),
						),
					)),
				},
			},
		},
		"a secret with invalid data should set condition to pending and wait for re-sync": {
			certificateRequest: baseCR.DeepCopy(),
			builder: &testpkg.Builder{
				KubeObjects: []runtime.Object{badDataSecret},
				CertManagerObjects: []runtime.Object{baseCR.DeepCopy(),
					gen.IssuerFrom(baseIssuer.DeepCopy(),
						gen.SetIssuerCA(cmapi.CAIssuer{SecretName: badDataSecret.Name}),
					),
				},
				ExpectedEvents: []string{
					"Normal SecretInvalidData Failed to parse signing CA keypair from secret default-unit-test-ns/root-ca-secret: error decoding private key PEM block",
				},
				ExpectedActions: []testpkg.Action{
					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
						cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
						"status",
						gen.DefaultTestNamespace,
						gen.CertificateRequestFrom(baseCR.DeepCopy(),
							gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
								Type:               cmapi.CertificateRequestConditionReady,
								Status:             cmmeta.ConditionFalse,
								Reason:             cmapi.CertificateRequestReasonPending,
								Message:            "Failed to parse signing CA keypair from secret default-unit-test-ns/root-ca-secret: error decoding private key PEM block",
								LastTransitionTime: &metaFixedClockStart,
							}),
						),
					)),
				},
			},
		},
		"a CertificateRequest that transiently fails a secret lookup should backoff error to retry": {
			certificateRequest: baseCR.DeepCopy(),
			builder: &testpkg.Builder{
				KubeObjects:        []runtime.Object{rsaCASecret},
				CertManagerObjects: []runtime.Object{baseCR.DeepCopy(), baseIssuer.DeepCopy()},
				ExpectedEvents: []string{
					`Normal SecretGetError Failed to get certificate key pair from secret default-unit-test-ns/root-ca-secret: this is a network error`,
				},
				ExpectedActions: []testpkg.Action{
					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
						cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
						"status",
						gen.DefaultTestNamespace,
						gen.CertificateRequestFrom(baseCR,
							gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
								Type:               cmapi.CertificateRequestConditionReady,
								Status:             cmmeta.ConditionFalse,
								Reason:             cmapi.CertificateRequestReasonPending,
								Message:            "Failed to get certificate key pair from secret default-unit-test-ns/root-ca-secret: this is a network error",
								LastTransitionTime: &metaFixedClockStart,
							}),
						),
					)),
				},
			},
			fakeLister: &testlisters.FakeSecretLister{
				SecretsFn: func(namespace string) clientcorev1.SecretNamespaceLister {
					return &testlisters.FakeSecretNamespaceLister{
						GetFn: func(name string) (ret *corev1.Secret, err error) {
							return nil, errors.New("this is a network error")
						},
					}
				},
			},
			expectedErr: true,
		},
		"should exit nil and set status pending if referenced issuer is not ready": {
			certificateRequest: baseCR.DeepCopy(),
			builder: &testpkg.Builder{
				KubeObjects: []runtime.Object{rsaCASecret},
				CertManagerObjects: []runtime.Object{baseCR.DeepCopy(),
					gen.Issuer(baseIssuer.DeepCopy().Name,
						gen.SetIssuerCA(cmapi.CAIssuer{}),
					)},
				ExpectedEvents: []string{
					"Normal IssuerNotReady Referenced issuer does not have a Ready status condition",
				},
				ExpectedActions: []testpkg.Action{
					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
						cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
						"status",
						gen.DefaultTestNamespace,
						gen.CertificateRequestFrom(baseCR,
							gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
								Type:               cmapi.CertificateRequestConditionReady,
								Status:             cmmeta.ConditionFalse,
								Reason:             "Pending",
								Message:            "Referenced issuer does not have a Ready status condition",
								LastTransitionTime: &metaFixedClockStart,
							}),
						),
					)),
				},
			},
		},
		"a secret that fails to sign due to failing to generate the certificate template should set condition to failed": {
			certificateRequest: baseCR.DeepCopy(),
			templateGenerator: func(*cmapi.CertificateRequest) (*x509.Certificate, error) {
				return nil, errors.New("this is a template generate error")
			},
			builder: &testpkg.Builder{
				KubeObjects:        []runtime.Object{rsaCASecret},
				CertManagerObjects: []runtime.Object{baseCR.DeepCopy(), baseIssuer.DeepCopy()},
				ExpectedEvents: []string{
					"Warning SigningError Error generating certificate template: this is a template generate error",
				},
				ExpectedActions: []testpkg.Action{
					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
						cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
						"status",
						gen.DefaultTestNamespace,
						gen.CertificateRequestFrom(baseCR.DeepCopy(),
							gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
								Type:               cmapi.CertificateRequestConditionReady,
								Status:             cmmeta.ConditionFalse,
								Reason:             cmapi.CertificateRequestReasonFailed,
								Message:            "Error generating certificate template: this is a template generate error",
								LastTransitionTime: &metaFixedClockStart,
							}),
							gen.SetCertificateRequestFailureTime(metaFixedClockStart),
						),
					)),
				},
			},
		},
		"a successful signinig should set condition to Ready": {
			certificateRequest: baseCR.DeepCopy(),
			templateGenerator: func(cr *cmapi.CertificateRequest) (*x509.Certificate, error) {
				_, err := pki.GenerateTemplateFromCertificateRequest(cr)
				if err != nil {
					return nil, err
				}

				return template, nil
			},
			builder: &testpkg.Builder{
				KubeObjects:        []runtime.Object{rsaCASecret},
				CertManagerObjects: []runtime.Object{baseCR.DeepCopy(), baseIssuer.DeepCopy()},
				ExpectedEvents: []string{
					"Normal CertificateIssued Certificate fetched from issuer successfully",
				},
				ExpectedActions: []testpkg.Action{
					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
						cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
						"status",
						gen.DefaultTestNamespace,
						gen.CertificateRequestFrom(baseCR,
							gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
								Type:               cmapi.CertificateRequestConditionReady,
								Status:             cmmeta.ConditionTrue,
								Reason:             cmapi.CertificateRequestReasonIssued,
								Message:            "Certificate fetched from issuer successfully",
								LastTransitionTime: &metaFixedClockStart,
							}),
							gen.SetCertificateRequestCA(rsaPEMCert),
							gen.SetCertificateRequestCertificate(certPEM),
						),
					)),
				},
			},
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			fixedClock.SetTime(fixedClockStart)
			test.builder.Clock = fixedClock
			runTest(t, test)
		})
	}
}

type testT struct {
	builder            *testpkg.Builder
	certificateRequest *cmapi.CertificateRequest
	templateGenerator  templateGenerator

	expectedErr bool

	fakeLister *testlisters.FakeSecretLister
}

func runTest(t *testing.T, test testT) {
	test.builder.T = t
	test.builder.Init()
	defer test.builder.Stop()

	ca := NewCA(test.builder.Context)

	if test.fakeLister != nil {
		ca.secretsLister = test.fakeLister
	}

	if test.templateGenerator != nil {
		ca.templateGenerator = test.templateGenerator
	}

	controller := certificaterequests.New(apiutil.IssuerCA, ca)
	controller.Register(test.builder.Context)
	test.builder.Start()

	err := controller.Sync(context.Background(), test.certificateRequest)
	if err != nil && !test.expectedErr {
		t.Errorf("expected to not get an error, but got: %v", err)
	}
	if err == nil && test.expectedErr {
		t.Errorf("expected to get an error but did not get one")
	}

	test.builder.CheckAndFinish(err)
}
<commit_msg>spelling: signing<commit_after>/*
Copyright 2019 The Jetstack cert-manager contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package ca

import (
	"bytes"
	"context"
	"crypto"
	"crypto/rand"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/asn1"
	"encoding/pem"
	"errors"
	"testing"
	"time"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	clientcorev1 "k8s.io/client-go/listers/core/v1"
	coretesting "k8s.io/client-go/testing"
	fakeclock "k8s.io/utils/clock/testing"

	apiutil "github.com/jetstack/cert-manager/pkg/api/util"
	"github.com/jetstack/cert-manager/pkg/apis/certmanager"
	cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2"
	cmmeta "github.com/jetstack/cert-manager/pkg/apis/meta/v1"
	"github.com/jetstack/cert-manager/pkg/controller/certificaterequests"
	testpkg "github.com/jetstack/cert-manager/pkg/controller/test"
	"github.com/jetstack/cert-manager/pkg/util/pki"
	"github.com/jetstack/cert-manager/test/unit/gen"
	testlisters "github.com/jetstack/cert-manager/test/unit/listers"
)

var (
	fixedClockStart = time.Now()
	fixedClock      = fakeclock.NewFakeClock(fixedClockStart)
)

func generateCSR(t *testing.T, secretKey crypto.Signer) []byte {
	asn1Subj, _ := asn1.Marshal(pkix.Name{
		CommonName: "test",
	}.ToRDNSequence())
	template := x509.CertificateRequest{
		RawSubject:         asn1Subj,
		SignatureAlgorithm: x509.SHA256WithRSA,
	}

	csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, secretKey)
	if err != nil {
		t.Error(err)
		t.FailNow()
	}

	csr := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes})

	return csr
}

func generateSelfSignedCertFromCR(t *testing.T, cr *cmapi.CertificateRequest, key crypto.Signer,
	duration time.Duration) (*x509.Certificate, []byte) {
	template, err := pki.GenerateTemplateFromCertificateRequest(cr)
	if err != nil {
		t.Errorf("error generating template: %v", err)
	}

	derBytes, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
	if err != nil {
		t.Errorf("error signing cert: %v", err)
		t.FailNow()
	}

	pemByteBuffer := bytes.NewBuffer([]byte{})
	err = pem.Encode(pemByteBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
	if err != nil {
		t.Errorf("failed to encode cert: %v", err)
		t.FailNow()
	}

	return template, pemByteBuffer.Bytes()
}

func TestSign(t *testing.T) {
	baseIssuer := gen.Issuer("test-issuer",
		gen.SetIssuerCA(cmapi.CAIssuer{SecretName: "root-ca-secret"}),
		gen.AddIssuerCondition(cmapi.IssuerCondition{
			Type:   cmapi.IssuerConditionReady,
			Status: cmmeta.ConditionTrue,
		}),
	)

	// Build root RSA CA
	skRSA, err := pki.GenerateRSAPrivateKey(2048)
	if err != nil {
		t.Error(err)
		t.FailNow()
	}

	skRSAPEM := pki.EncodePKCS1PrivateKey(skRSA)
	rsaCSR := generateCSR(t, skRSA)

	baseCR := gen.CertificateRequest("test-cr",
		gen.SetCertificateRequestIsCA(true),
		gen.SetCertificateRequestCSR(rsaCSR),
		gen.SetCertificateRequestIssuer(cmmeta.ObjectReference{
			Name:  baseIssuer.DeepCopy().Name,
			Group: certmanager.GroupName,
			Kind:  "Issuer",
		}),
		gen.SetCertificateRequestDuration(&metav1.Duration{Duration: time.Hour * 24 * 60}),
	)

	// generate a self signed root ca valid for 60d
	_, rsaPEMCert := generateSelfSignedCertFromCR(t, baseCR, skRSA, time.Hour*24*60)
	rsaCASecret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "root-ca-secret",
			Namespace: gen.DefaultTestNamespace,
		},
		Data: map[string][]byte{
			corev1.TLSPrivateKeyKey: skRSAPEM,
			corev1.TLSCertKey:       rsaPEMCert,
		},
	}

	badDataSecret := rsaCASecret.DeepCopy()
	badDataSecret.Data[corev1.TLSPrivateKeyKey] = []byte("bad key")

	template, err := pki.GenerateTemplateFromCertificateRequest(baseCR)
	if err != nil {
		t.Error(err)
		t.FailNow()
	}
	certPEM, _, err := pki.SignCSRTemplate([]*x509.Certificate{template}, skRSA, template)
	if err != nil {
		t.Error(err)
		t.FailNow()
	}

	metaFixedClockStart := metav1.NewTime(fixedClockStart)
	tests := map[string]testT{
		"a missing CA key pair should set the condition to pending and wait for a re-sync": {
			certificateRequest: baseCR.DeepCopy(),
			builder: &testpkg.Builder{
				KubeObjects:        []runtime.Object{},
				CertManagerObjects: []runtime.Object{baseCR.DeepCopy(), baseIssuer.DeepCopy()},
				ExpectedEvents: []string{
					`Normal SecretMissing Referenced secret default-unit-test-ns/root-ca-secret not found: secret "root-ca-secret" not found`,
				},
				ExpectedActions: []testpkg.Action{
					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
						cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
						"status",
						gen.DefaultTestNamespace,
						gen.CertificateRequestFrom(baseCR.DeepCopy(),
							gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
								Type:               cmapi.CertificateRequestConditionReady,
								Status:             cmmeta.ConditionFalse,
								Reason:             cmapi.CertificateRequestReasonPending,
								Message:            `Referenced secret default-unit-test-ns/root-ca-secret not found: secret "root-ca-secret" not found`,
								LastTransitionTime: &metaFixedClockStart,
							}),
						),
					)),
				},
			},
		},
		"a secret with invalid data should set condition to pending and wait for re-sync": {
			certificateRequest: baseCR.DeepCopy(),
			builder: &testpkg.Builder{
				KubeObjects: []runtime.Object{badDataSecret},
				CertManagerObjects: []runtime.Object{baseCR.DeepCopy(),
					gen.IssuerFrom(baseIssuer.DeepCopy(),
						gen.SetIssuerCA(cmapi.CAIssuer{SecretName: badDataSecret.Name}),
					),
				},
				ExpectedEvents: []string{
					"Normal SecretInvalidData Failed to parse signing CA keypair from secret default-unit-test-ns/root-ca-secret: error decoding private key PEM block",
				},
				ExpectedActions: []testpkg.Action{
					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
						cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
						"status",
						gen.DefaultTestNamespace,
						gen.CertificateRequestFrom(baseCR.DeepCopy(),
							gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
								Type:               cmapi.CertificateRequestConditionReady,
								Status:             cmmeta.ConditionFalse,
								Reason:             cmapi.CertificateRequestReasonPending,
								Message:            "Failed to parse signing CA keypair from secret default-unit-test-ns/root-ca-secret: error decoding private key PEM block",
								LastTransitionTime: &metaFixedClockStart,
							}),
						),
					)),
				},
			},
		},
		"a CertificateRequest that transiently fails a secret lookup should backoff error to retry": {
			certificateRequest: baseCR.DeepCopy(),
			builder: &testpkg.Builder{
				KubeObjects:        []runtime.Object{rsaCASecret},
				CertManagerObjects: []runtime.Object{baseCR.DeepCopy(), baseIssuer.DeepCopy()},
				ExpectedEvents: []string{
					`Normal SecretGetError Failed to get certificate key pair from secret default-unit-test-ns/root-ca-secret: this is a network error`,
				},
				ExpectedActions: []testpkg.Action{
					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
						cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
						"status",
						gen.DefaultTestNamespace,
						gen.CertificateRequestFrom(baseCR,
							gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
								Type:               cmapi.CertificateRequestConditionReady,
								Status:             cmmeta.ConditionFalse,
								Reason:             cmapi.CertificateRequestReasonPending,
								Message:            "Failed to get certificate key pair from secret default-unit-test-ns/root-ca-secret: this is a network error",
								LastTransitionTime: &metaFixedClockStart,
							}),
						),
					)),
				},
			},
			fakeLister: &testlisters.FakeSecretLister{
				SecretsFn: func(namespace string) clientcorev1.SecretNamespaceLister {
					return &testlisters.FakeSecretNamespaceLister{
						GetFn: func(name string) (ret *corev1.Secret, err error) {
							return nil, errors.New("this is a network error")
						},
					}
				},
			},
			expectedErr: true,
		},
		"should exit nil and set status pending if referenced issuer is not ready": {
			certificateRequest: baseCR.DeepCopy(),
			builder: &testpkg.Builder{
				KubeObjects: []runtime.Object{rsaCASecret},
				CertManagerObjects: []runtime.Object{baseCR.DeepCopy(),
					gen.Issuer(baseIssuer.DeepCopy().Name,
						gen.SetIssuerCA(cmapi.CAIssuer{}),
					)},
				ExpectedEvents: []string{
					"Normal IssuerNotReady Referenced issuer does not have a Ready status condition",
				},
				ExpectedActions: []testpkg.Action{
					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
						cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
						"status",
						gen.DefaultTestNamespace,
						gen.CertificateRequestFrom(baseCR,
							gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
								Type:               cmapi.CertificateRequestConditionReady,
								Status:             cmmeta.ConditionFalse,
								Reason:             "Pending",
								Message:            "Referenced issuer does not have a Ready status condition",
								LastTransitionTime: &metaFixedClockStart,
							}),
						),
					)),
				},
			},
		},
		"a secret that fails to sign due to failing to generate the certificate template should set condition to failed": {
			certificateRequest: baseCR.DeepCopy(),
			templateGenerator: func(*cmapi.CertificateRequest) (*x509.Certificate, error) {
				return nil, errors.New("this is a template generate error")
			},
			builder: &testpkg.Builder{
				KubeObjects:        []runtime.Object{rsaCASecret},
				CertManagerObjects: []runtime.Object{baseCR.DeepCopy(), baseIssuer.DeepCopy()},
				ExpectedEvents: []string{
					"Warning SigningError Error generating certificate template: this is a template generate error",
				},
				ExpectedActions: []testpkg.Action{
					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
						cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
						"status",
						gen.DefaultTestNamespace,
						gen.CertificateRequestFrom(baseCR.DeepCopy(),
							gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
								Type:               cmapi.CertificateRequestConditionReady,
								Status:             cmmeta.ConditionFalse,
								Reason:             cmapi.CertificateRequestReasonFailed,
								Message:            "Error generating certificate template: this is a template generate error",
								LastTransitionTime: &metaFixedClockStart,
							}),
							gen.SetCertificateRequestFailureTime(metaFixedClockStart),
						),
					)),
				},
			},
		},
		"a successful signing should set condition to Ready": {
			certificateRequest: baseCR.DeepCopy(),
			templateGenerator: func(cr *cmapi.CertificateRequest) (*x509.Certificate, error) {
				_, err := pki.GenerateTemplateFromCertificateRequest(cr)
				if err != nil {
					return nil, err
				}

				return template, nil
			},
			builder: &testpkg.Builder{
				KubeObjects:        []runtime.Object{rsaCASecret},
				CertManagerObjects: []runtime.Object{baseCR.DeepCopy(), baseIssuer.DeepCopy()},
				ExpectedEvents: []string{
					"Normal CertificateIssued Certificate fetched from issuer successfully",
				},
				ExpectedActions: []testpkg.Action{
					testpkg.NewAction(coretesting.NewUpdateSubresourceAction(
						cmapi.SchemeGroupVersion.WithResource("certificaterequests"),
						"status",
						gen.DefaultTestNamespace,
						gen.CertificateRequestFrom(baseCR,
							gen.SetCertificateRequestStatusCondition(cmapi.CertificateRequestCondition{
								Type:               cmapi.CertificateRequestConditionReady,
								Status:             cmmeta.ConditionTrue,
								Reason:             cmapi.CertificateRequestReasonIssued,
								Message:            "Certificate fetched from issuer successfully",
								LastTransitionTime: &metaFixedClockStart,
							}),
							gen.SetCertificateRequestCA(rsaPEMCert),
							gen.SetCertificateRequestCertificate(certPEM),
						),
					)),
				},
			},
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			fixedClock.SetTime(fixedClockStart)
			test.builder.Clock = fixedClock
			runTest(t, test)
		})
	}
}

type testT struct {
	builder            *testpkg.Builder
	certificateRequest *cmapi.CertificateRequest
	templateGenerator  templateGenerator

	expectedErr bool

	fakeLister *testlisters.FakeSecretLister
}

func runTest(t *testing.T, test testT) {
	test.builder.T = t
	test.builder.Init()
	defer test.builder.Stop()

	ca := NewCA(test.builder.Context)

	if test.fakeLister != nil {
		ca.secretsLister = test.fakeLister
	}

	if test.templateGenerator != nil {
		ca.templateGenerator = test.templateGenerator
	}

	controller := certificaterequests.New(apiutil.IssuerCA, ca)
	controller.Register(test.builder.Context)
	test.builder.Start()

	err := controller.Sync(context.Background(), test.certificateRequest)
	if err != nil && !test.expectedErr {
		t.Errorf("expected to not get an error, but got: %v", err)
	}
	if err == nil && test.expectedErr {
		t.Errorf("expected to get an error but did not get one")
	}

	test.builder.CheckAndFinish(err)
}
<|endoftext|>


<commit_before>/*
Copyright 2017 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package aggregator

import (
	"fmt"
	"net/http"
	"strings"
	"sync"
	"time"

	restful "github.com/emicklei/go-restful"
	"github.com/go-openapi/spec"

	"k8s.io/apiserver/pkg/server"
	"k8s.io/kube-aggregator/pkg/apis/apiregistration"
	"k8s.io/kube-openapi/pkg/aggregator"
	"k8s.io/kube-openapi/pkg/builder"
	"k8s.io/kube-openapi/pkg/common"
	"k8s.io/kube-openapi/pkg/handler"
)

// SpecAggregator calls out to http handlers of APIServices and merges specs. It keeps state of the last
// known specs including the http etag.
type SpecAggregator interface {
	AddUpdateAPIService(handler http.Handler, apiService *apiregistration.APIService) error
	UpdateAPIServiceSpec(apiServiceName string, spec *spec.Swagger, etag string) error
	RemoveAPIServiceSpec(apiServiceName string) error
	GetAPIServiceInfo(apiServiceName string) (handler http.Handler, etag string, exists bool)
	GetAPIServiceNames() []string
}

const (
	aggregatorUser                = "system:aggregator"
	specDownloadTimeout           = 60 * time.Second
	localDelegateChainNamePrefix  = "k8s_internal_local_delegation_chain_"
	localDelegateChainNamePattern = localDelegateChainNamePrefix + "%010d"

	// A randomly generated UUID to differentiate local and remote eTags.
	locallyGeneratedEtagPrefix = "\"6E8F849B434D4B98A569B9D7718876E9-"
)

// IsLocalAPIService returns true for local specs from delegates.
func IsLocalAPIService(apiServiceName string) bool {
	return strings.HasPrefix(apiServiceName, localDelegateChainNamePrefix)
}

// GetAPIServicesName returns the names of APIServices recorded in specAggregator.openAPISpecs.
// We use this function to pass the names of local APIServices to the controller in this package,
// so that the controller can periodically sync the OpenAPI spec from delegation API servers.
func (s *specAggregator) GetAPIServiceNames() []string {
	names := make([]string, len(s.openAPISpecs))
	for key := range s.openAPISpecs {
		names = append(names, key)
	}
	return names
}

// BuildAndRegisterAggregator registered OpenAPI aggregator handler. This function is not thread safe as it only being called on startup.
func BuildAndRegisterAggregator(downloader *Downloader, delegationTarget server.DelegationTarget, webServices []*restful.WebService,
	config *common.Config, pathHandler common.PathHandler) (SpecAggregator, error) {
	s := &specAggregator{
		openAPISpecs: map[string]*openAPISpecInfo{},
	}

	i := 0
	// Build Aggregator's spec
	aggregatorOpenAPISpec, err := builder.BuildOpenAPISpec(webServices, config)
	if err != nil {
		return nil, err
	}

	// Reserving non-name spec for aggregator's Spec.
	s.addLocalSpec(aggregatorOpenAPISpec, nil, fmt.Sprintf(localDelegateChainNamePattern, i), "")
	i++
	for delegate := delegationTarget; delegate != nil; delegate = delegate.NextDelegate() {
		handler := delegate.UnprotectedHandler()
		if handler == nil {
			continue
		}
		delegateSpec, etag, _, err := downloader.Download(handler, "")
		if err != nil {
			return nil, err
		}
		if delegateSpec == nil {
			continue
		}
		s.addLocalSpec(delegateSpec, handler, fmt.Sprintf(localDelegateChainNamePattern, i), etag)
		i++
	}

	// Build initial spec to serve.
	specToServe, err := s.buildOpenAPISpec()
	if err != nil {
		return nil, err
	}

	// Install handler
	s.openAPIVersionedService, err = handler.RegisterOpenAPIVersionedService(
		specToServe, "/openapi/v2", pathHandler)
	if err != nil {
		return nil, err
	}

	return s, nil
}

type specAggregator struct {
	// mutex protects all members of this struct.
	rwMutex sync.RWMutex

	// Map of API Services' OpenAPI specs by their name
	openAPISpecs map[string]*openAPISpecInfo

	// provided for dynamic OpenAPI spec
	openAPIVersionedService *handler.OpenAPIService
}

var _ SpecAggregator = &specAggregator{}

// This function is not thread safe as it only being called on startup.
func (s *specAggregator) addLocalSpec(spec *spec.Swagger, localHandler http.Handler, name, etag string) {
	localAPIService := apiregistration.APIService{}
	localAPIService.Name = name
	s.openAPISpecs[name] = &openAPISpecInfo{
		etag:       etag,
		apiService: localAPIService,
		handler:    localHandler,
		spec:       spec,
	}
}

// openAPISpecInfo is used to store OpenAPI spec with its priority.
// It can be used to sort specs with their priorities.
type openAPISpecInfo struct {
	apiService apiregistration.APIService

	// Specification of this API Service. If null then the spec is not loaded yet.
	spec    *spec.Swagger
	handler http.Handler
	etag    string
}

// buildOpenAPISpec aggregates all OpenAPI specs.  It is not thread-safe. The caller is responsible to hold proper locks.
func (s *specAggregator) buildOpenAPISpec() (specToReturn *spec.Swagger, err error) {
	specs := []openAPISpecInfo{}
	for _, specInfo := range s.openAPISpecs {
		if specInfo.spec == nil {
			continue
		}
		specs = append(specs, *specInfo)
	}
	if len(specs) == 0 {
		return &spec.Swagger{}, nil
	}
	sortByPriority(specs)
	for _, specInfo := range specs {
		if specToReturn == nil {
			specToReturn = &spec.Swagger{}
			*specToReturn = *specInfo.spec
			// Paths and Definitions are set by MergeSpecsIgnorePathConflict
			specToReturn.Paths = nil
			specToReturn.Definitions = nil
		}
		if err := aggregator.MergeSpecsIgnorePathConflict(specToReturn, specInfo.spec); err != nil {
			return nil, err
		}
	}

	return specToReturn, nil
}

// updateOpenAPISpec aggregates all OpenAPI specs.  It is not thread-safe. The caller is responsible to hold proper locks.
func (s *specAggregator) updateOpenAPISpec() error {
	if s.openAPIVersionedService == nil {
		return nil
	}
	specToServe, err := s.buildOpenAPISpec()
	if err != nil {
		return err
	}
	return s.openAPIVersionedService.UpdateSpec(specToServe)
}

// tryUpdatingServiceSpecs tries updating openAPISpecs map with specified specInfo, and keeps the map intact
// if the update fails.
func (s *specAggregator) tryUpdatingServiceSpecs(specInfo *openAPISpecInfo) error {
	if specInfo == nil {
		return fmt.Errorf("invalid input: specInfo must be non-nil")
	}
	orgSpecInfo, exists := s.openAPISpecs[specInfo.apiService.Name]
	// Skip aggregation if OpenAPI spec didn't change
	if exists && orgSpecInfo != nil && orgSpecInfo.etag == specInfo.etag {
		return nil
	}
	s.openAPISpecs[specInfo.apiService.Name] = specInfo
	if err := s.updateOpenAPISpec(); err != nil {
		if exists {
			s.openAPISpecs[specInfo.apiService.Name] = orgSpecInfo
		} else {
			delete(s.openAPISpecs, specInfo.apiService.Name)
		}
		return err
	}
	return nil
}

// tryDeleteServiceSpecs tries delete specified specInfo from openAPISpecs map, and keeps the map intact
// if the update fails.
func (s *specAggregator) tryDeleteServiceSpecs(apiServiceName string) error {
	orgSpecInfo, exists := s.openAPISpecs[apiServiceName]
	if !exists {
		return nil
	}
	delete(s.openAPISpecs, apiServiceName)
	if err := s.updateOpenAPISpec(); err != nil {
		s.openAPISpecs[apiServiceName] = orgSpecInfo
		return err
	}
	return nil
}

// UpdateAPIServiceSpec updates the api service's OpenAPI spec. It is thread safe.
func (s *specAggregator) UpdateAPIServiceSpec(apiServiceName string, spec *spec.Swagger, etag string) error {
	s.rwMutex.Lock()
	defer s.rwMutex.Unlock()

	specInfo, existingService := s.openAPISpecs[apiServiceName]
	if !existingService {
		return fmt.Errorf("APIService %q does not exists", apiServiceName)
	}

	// For APIServices (non-local) specs, only merge their /apis/ prefixed endpoint as it is the only paths
	// proxy handler delegates.
	if specInfo.apiService.Spec.Service != nil {
		spec = aggregator.FilterSpecByPathsWithoutSideEffects(spec, []string{"/apis/"})
	}

	return s.tryUpdatingServiceSpecs(&openAPISpecInfo{
		apiService: specInfo.apiService,
		spec:       spec,
		handler:    specInfo.handler,
		etag:       etag,
	})
}

// AddUpdateAPIService adds or updates the api service. It is thread safe.
func (s *specAggregator) AddUpdateAPIService(handler http.Handler, apiService *apiregistration.APIService) error {
	s.rwMutex.Lock()
	defer s.rwMutex.Unlock()

	if apiService.Spec.Service == nil {
		// All local specs should be already aggregated using local delegate chain
		return nil
	}

	newSpec := &openAPISpecInfo{
		apiService: *apiService,
		handler:    handler,
	}
	if specInfo, existingService := s.openAPISpecs[apiService.Name]; existingService {
		newSpec.etag = specInfo.etag
		newSpec.spec = specInfo.spec
	}
	return s.tryUpdatingServiceSpecs(newSpec)
}

// RemoveAPIServiceSpec removes an api service from OpenAPI aggregation. If it does not exist, no error is returned.
// It is thread safe.
func (s *specAggregator) RemoveAPIServiceSpec(apiServiceName string) error {
	s.rwMutex.Lock()
	defer s.rwMutex.Unlock()

	if _, existingService := s.openAPISpecs[apiServiceName]; !existingService {
		return nil
	}

	return s.tryDeleteServiceSpecs(apiServiceName)
}

// GetAPIServiceSpec returns api service spec info
func (s *specAggregator) GetAPIServiceInfo(apiServiceName string) (handler http.Handler, etag string, exists bool) {
	s.rwMutex.RLock()
	defer s.rwMutex.RUnlock()

	if info, existingService := s.openAPISpecs[apiServiceName]; existingService {
		return info.handler, info.etag, true
	}
	return nil, "", false
}
<commit_msg>kube-aggregator: update existing openapi spec info if pre-existing<commit_after>/*
Copyright 2017 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package aggregator

import (
	"fmt"
	"net/http"
	"strings"
	"sync"
	"time"

	restful "github.com/emicklei/go-restful"
	"github.com/go-openapi/spec"

	"k8s.io/apiserver/pkg/server"
	"k8s.io/kube-aggregator/pkg/apis/apiregistration"
	"k8s.io/kube-openapi/pkg/aggregator"
	"k8s.io/kube-openapi/pkg/builder"
	"k8s.io/kube-openapi/pkg/common"
	"k8s.io/kube-openapi/pkg/handler"
)

// SpecAggregator calls out to http handlers of APIServices and merges specs. It keeps state of the last
// known specs including the http etag.
type SpecAggregator interface {
	AddUpdateAPIService(handler http.Handler, apiService *apiregistration.APIService) error
	UpdateAPIServiceSpec(apiServiceName string, spec *spec.Swagger, etag string) error
	RemoveAPIServiceSpec(apiServiceName string) error
	GetAPIServiceInfo(apiServiceName string) (handler http.Handler, etag string, exists bool)
	GetAPIServiceNames() []string
}

const (
	aggregatorUser                = "system:aggregator"
	specDownloadTimeout           = 60 * time.Second
	localDelegateChainNamePrefix  = "k8s_internal_local_delegation_chain_"
	localDelegateChainNamePattern = localDelegateChainNamePrefix + "%010d"

	// A randomly generated UUID to differentiate local and remote eTags.
	locallyGeneratedEtagPrefix = "\"6E8F849B434D4B98A569B9D7718876E9-"
)

// IsLocalAPIService returns true for local specs from delegates.
func IsLocalAPIService(apiServiceName string) bool {
	return strings.HasPrefix(apiServiceName, localDelegateChainNamePrefix)
}

// GetAPIServicesName returns the names of APIServices recorded in specAggregator.openAPISpecs.
// We use this function to pass the names of local APIServices to the controller in this package,
// so that the controller can periodically sync the OpenAPI spec from delegation API servers.
func (s *specAggregator) GetAPIServiceNames() []string {
	names := make([]string, len(s.openAPISpecs))
	for key := range s.openAPISpecs {
		names = append(names, key)
	}
	return names
}

// BuildAndRegisterAggregator registered OpenAPI aggregator handler. This function is not thread safe as it only being called on startup.
func BuildAndRegisterAggregator(downloader *Downloader, delegationTarget server.DelegationTarget, webServices []*restful.WebService,
	config *common.Config, pathHandler common.PathHandler) (SpecAggregator, error) {
	s := &specAggregator{
		openAPISpecs: map[string]*openAPISpecInfo{},
	}

	i := 0
	// Build Aggregator's spec
	aggregatorOpenAPISpec, err := builder.BuildOpenAPISpec(webServices, config)
	if err != nil {
		return nil, err
	}

	// Reserving non-name spec for aggregator's Spec.
	s.addLocalSpec(aggregatorOpenAPISpec, nil, fmt.Sprintf(localDelegateChainNamePattern, i), "")
	i++
	for delegate := delegationTarget; delegate != nil; delegate = delegate.NextDelegate() {
		handler := delegate.UnprotectedHandler()
		if handler == nil {
			continue
		}
		delegateSpec, etag, _, err := downloader.Download(handler, "")
		if err != nil {
			return nil, err
		}
		if delegateSpec == nil {
			continue
		}
		s.addLocalSpec(delegateSpec, handler, fmt.Sprintf(localDelegateChainNamePattern, i), etag)
		i++
	}

	// Build initial spec to serve.
	specToServe, err := s.buildOpenAPISpec()
	if err != nil {
		return nil, err
	}

	// Install handler
	s.openAPIVersionedService, err = handler.RegisterOpenAPIVersionedService(
		specToServe, "/openapi/v2", pathHandler)
	if err != nil {
		return nil, err
	}

	return s, nil
}

type specAggregator struct {
	// mutex protects all members of this struct.
	rwMutex sync.RWMutex

	// Map of API Services' OpenAPI specs by their name
	openAPISpecs map[string]*openAPISpecInfo

	// provided for dynamic OpenAPI spec
	openAPIVersionedService *handler.OpenAPIService
}

var _ SpecAggregator = &specAggregator{}

// This function is not thread safe as it only being called on startup.
func (s *specAggregator) addLocalSpec(spec *spec.Swagger, localHandler http.Handler, name, etag string) {
	localAPIService := apiregistration.APIService{}
	localAPIService.Name = name
	s.openAPISpecs[name] = &openAPISpecInfo{
		etag:       etag,
		apiService: localAPIService,
		handler:    localHandler,
		spec:       spec,
	}
}

// openAPISpecInfo is used to store OpenAPI spec with its priority.
// It can be used to sort specs with their priorities.
type openAPISpecInfo struct {
	apiService apiregistration.APIService

	// Specification of this API Service. If null then the spec is not loaded yet.
	spec    *spec.Swagger
	handler http.Handler
	etag    string
}

// buildOpenAPISpec aggregates all OpenAPI specs.  It is not thread-safe. The caller is responsible to hold proper locks.
func (s *specAggregator) buildOpenAPISpec() (specToReturn *spec.Swagger, err error) {
	specs := []openAPISpecInfo{}
	for _, specInfo := range s.openAPISpecs {
		if specInfo.spec == nil {
			continue
		}
		specs = append(specs, *specInfo)
	}
	if len(specs) == 0 {
		return &spec.Swagger{}, nil
	}
	sortByPriority(specs)
	for _, specInfo := range specs {
		if specToReturn == nil {
			specToReturn = &spec.Swagger{}
			*specToReturn = *specInfo.spec
			// Paths and Definitions are set by MergeSpecsIgnorePathConflict
			specToReturn.Paths = nil
			specToReturn.Definitions = nil
		}
		if err := aggregator.MergeSpecsIgnorePathConflict(specToReturn, specInfo.spec); err != nil {
			return nil, err
		}
	}

	return specToReturn, nil
}

// updateOpenAPISpec aggregates all OpenAPI specs.  It is not thread-safe. The caller is responsible to hold proper locks.
func (s *specAggregator) updateOpenAPISpec() error {
	if s.openAPIVersionedService == nil {
		return nil
	}
	specToServe, err := s.buildOpenAPISpec()
	if err != nil {
		return err
	}
	return s.openAPIVersionedService.UpdateSpec(specToServe)
}

// tryUpdatingServiceSpecs tries updating openAPISpecs map with specified specInfo, and keeps the map intact
// if the update fails.
func (s *specAggregator) tryUpdatingServiceSpecs(specInfo *openAPISpecInfo) error {
	if specInfo == nil {
		return fmt.Errorf("invalid input: specInfo must be non-nil")
	}
	origSpecInfo, existedBefore := s.openAPISpecs[specInfo.apiService.Name]
	s.openAPISpecs[specInfo.apiService.Name] = specInfo

	// Skip aggregation if OpenAPI spec didn't change
	if existedBefore && origSpecInfo != nil && origSpecInfo.etag == specInfo.etag {
		return nil
	}
	if err := s.updateOpenAPISpec(); err != nil {
		if existedBefore {
			s.openAPISpecs[specInfo.apiService.Name] = origSpecInfo
		} else {
			delete(s.openAPISpecs, specInfo.apiService.Name)
		}
		return err
	}
	return nil
}

// tryDeleteServiceSpecs tries delete specified specInfo from openAPISpecs map, and keeps the map intact
// if the update fails.
func (s *specAggregator) tryDeleteServiceSpecs(apiServiceName string) error {
	orgSpecInfo, exists := s.openAPISpecs[apiServiceName]
	if !exists {
		return nil
	}
	delete(s.openAPISpecs, apiServiceName)
	if err := s.updateOpenAPISpec(); err != nil {
		s.openAPISpecs[apiServiceName] = orgSpecInfo
		return err
	}
	return nil
}

// UpdateAPIServiceSpec updates the api service's OpenAPI spec. It is thread safe.
func (s *specAggregator) UpdateAPIServiceSpec(apiServiceName string, spec *spec.Swagger, etag string) error {
	s.rwMutex.Lock()
	defer s.rwMutex.Unlock()

	specInfo, existingService := s.openAPISpecs[apiServiceName]
	if !existingService {
		return fmt.Errorf("APIService %q does not exists", apiServiceName)
	}

	// For APIServices (non-local) specs, only merge their /apis/ prefixed endpoint as it is the only paths
	// proxy handler delegates.
	if specInfo.apiService.Spec.Service != nil {
		spec = aggregator.FilterSpecByPathsWithoutSideEffects(spec, []string{"/apis/"})
	}

	return s.tryUpdatingServiceSpecs(&openAPISpecInfo{
		apiService: specInfo.apiService,
		spec:       spec,
		handler:    specInfo.handler,
		etag:       etag,
	})
}

// AddUpdateAPIService adds or updates the api service. It is thread safe.
func (s *specAggregator) AddUpdateAPIService(handler http.Handler, apiService *apiregistration.APIService) error {
	s.rwMutex.Lock()
	defer s.rwMutex.Unlock()

	if apiService.Spec.Service == nil {
		// All local specs should be already aggregated using local delegate chain
		return nil
	}

	newSpec := &openAPISpecInfo{
		apiService: *apiService,
		handler:    handler,
	}
	if specInfo, existingService := s.openAPISpecs[apiService.Name]; existingService {
		newSpec.etag = specInfo.etag
		newSpec.spec = specInfo.spec
	}
	return s.tryUpdatingServiceSpecs(newSpec)
}

// RemoveAPIServiceSpec removes an api service from OpenAPI aggregation. If it does not exist, no error is returned.
// It is thread safe.
func (s *specAggregator) RemoveAPIServiceSpec(apiServiceName string) error {
	s.rwMutex.Lock()
	defer s.rwMutex.Unlock()

	if _, existingService := s.openAPISpecs[apiServiceName]; !existingService {
		return nil
	}

	return s.tryDeleteServiceSpecs(apiServiceName)
}

// GetAPIServiceSpec returns api service spec info
func (s *specAggregator) GetAPIServiceInfo(apiServiceName string) (handler http.Handler, etag string, exists bool) {
	s.rwMutex.RLock()
	defer s.rwMutex.RUnlock()

	if info, existingService := s.openAPISpecs[apiServiceName]; existingService {
		return info.handler, info.etag, true
	}
	return nil, "", false
}
<|endoftext|>


<commit_before>package main

import (
	"log"
	"strings"

	"github.com/codegangsta/cli"
)

func doInit(c *cli.Context) {
	if err := SetConfig(); err != nil {
		log.Fatalf("failed to create a setting file\n", err)
	}
}

func doStart(c *cli.Context) {
	log.Println("[start] Initialization...")

	// read a configuration file
	config, err := GetConfig()
	if err != nil {
		log.Fatalf("at first, \"$ surelock-homes init\"\n", err)
	}

	// command option check
	cmdOptionSerial := c.String("serial")
	if cmdOptionSerial != "SERIAL PORT" {
		config.SerialPort.Serial = cmdOptionSerial
	}

	// twitter initial
	token := TwitterInit(config.Twitter)
	// serial initial
	serialObject, err := SerialInit(config.SerialPort)
	if err != nil {
		log.Fatalf("failed to open the serial port\n", err)
	}

	log.Println("[start] TwitterStreaming and BeaconScanning...")

	for {
		timestamp := make(chan string)
		go TwitterStreaming(timestamp, token, config.Twitter.ServerAccount)
		go BeaconScan(timestamp, config.Bluetooth)

		ts1 := <-timestamp
		ts2 := <-timestamp

		// door doesn't open when the difference exceeds the 5 minutes
		timediff := TimeDiff(ts1, ts2)
		if timediff >= 300 || timediff < 0 {
			continue
		}

		// send a open command
		err = SerialWrite(serialObject, "OC0")
		if err != nil {
			log.Fatalf("failed to write\n", err)
		}

		// twitter post
		message := strings.Join([]string{"@", config.Twitter.ClientAccount, " [from Surelock-Homes] The door has opened."}, "")
		err = TwitterPost(token, message)
		if err != nil {
			log.Fatalf("failed to post a tweet\n", err)
		}
	}
}
<commit_msg>add: action door close<commit_after>package main

import (
	"log"
	"strings"

	"github.com/codegangsta/cli"
)

func doInit(c *cli.Context) {
	if err := SetConfig(); err != nil {
		log.Fatalf("failed to create a setting file\n", err)
	}
}

func doStart(c *cli.Context) {
	log.Println("[start] Initialization...")

	// read a configuration file
	config, err := GetConfig()
	if err != nil {
		log.Fatalf("at first, \"$ surelock-homes init\"\n", err)
	}

	// command option check
	cmdOptionSerial := c.String("serial")
	if cmdOptionSerial != "SERIAL PORT" {
		config.SerialPort.Serial = cmdOptionSerial
	}

	// twitter initial
	token := TwitterInit(config.Twitter)
	// serial initial
	serialObject, err := SerialInit(config.SerialPort)
	if err != nil {
		log.Fatalf("failed to open the serial port\n", err)
	}

	log.Println("[start] TwitterStreaming and BeaconScanning...")

	for {
		timestamp := make(chan string)
		go TwitterStreaming(timestamp, token, config.Twitter.ServerAccount)
		go BeaconScan(timestamp, config.Bluetooth)

		ts1 := <-timestamp
		ts2 := <-timestamp

		// send a close command
		err = SerialWrite(serialObject, "OC1")
		if err != nil {
			log.Fatalf("failed to write\n", err)
		}

		// twitter post
		message := "[from Surelock-Homes] The door has closed."
		err = TwitterPost(token, message)
		if err != nil {
			log.Fatalf("failed to post a tweet\n", err)
		}

		// door doesn't open when the difference exceeds the 5 minutes
		timediff := TimeDiff(ts1, ts2)
		if timediff >= 300 || timediff < 0 {
			continue
		}

		// send a open command
		err = SerialWrite(serialObject, "OC0")
		if err != nil {
			log.Fatalf("failed to write\n", err)
		}

		// twitter post
		message = strings.Join([]string{"@", config.Twitter.ClientAccount, " [from Surelock-Homes] The door has opened."}, "")
		err = TwitterPost(token, message)
		if err != nil {
			log.Fatalf("failed to post a tweet\n", err)
		}
	}
}
<|endoftext|>


<commit_before>package cloudconfig

import (
	"errors"
	"io"
	"io/ioutil"
	"strings"

	"github.com/tmrts/flamingo/pkg/sys/identity"
	"github.com/tmrts/flamingo/pkg/sys/ssh"

	"gopkg.in/yaml.v2"
)

type WriteFile struct {
	Path        string
	Owner       string
	Permissions string
	Encoding    string

	Content string
}

var (
	ErrNotACloudConfigFile = errors.New("cloudconfig: not a cloud-config file")
)

func IsValid(rdr io.ReadCloser) error {
	buf, err := ioutil.ReadAll(rdr)
	if err != nil {
		return err
	}
	rdr.Close()

	if contents := string(buf); strings.HasPrefix(contents, "#cloud-config\n") != true {
		return ErrNotACloudConfigFile
	}

	return nil
}

type user struct {
	identity.User     `yaml:""`
	AuthorizedSSHKeys []string `yaml:"ssh-authorized-keys"`
	SSHImportID       string   `yaml:"ssh-import-id"`
}

type cloudConfig struct {
	RunCMD         []interface{}
	AuthorizedKeys []string          `yaml:"ssh_authorized_keys"`
	SSHKeyPairs    map[string]string `yaml:"ssh_keys"`
	Users          []identity.User   `yaml:"users"`
	Groups         []interface{}     `yaml:"groups"`
	Files          []WriteFile       `yaml:"write_files"`
}

// Digest is the parsed cloud-config file.
type Digest struct {
	Commands       [][]string
	Files          []WriteFile
	Groups         map[string][]string
	Users          map[string]identity.User
	AuthorizedKeys map[string][]ssh.Key
	SSHKeyPairs    []ssh.KeyPair
}

// Parse parses the given cloud-config file when it's path is given.
func Parse(rdr io.ReadCloser) (*Digest, error) {
	buf, err := ioutil.ReadAll(rdr)
	if err != nil {
		return nil, err
	}
	rdr.Close()

	var conf cloudConfig
	if err := yaml.Unmarshal(buf, &conf); err != nil {
		return nil, err
	}
	var c Digest

	c.Commands = parseCommands(conf.RunCMD)

	c.AuthorizedKeys = make(map[string][]ssh.Key)
	for _, key := range conf.AuthorizedKeys {
		c.AuthorizedKeys["root"] = append(c.AuthorizedKeys["root"], ssh.Key(key))
	}

	public_keys, private_keys := make(map[string]string), make(map[string]string)

	// TODO: Extend ssh key syntax beyond cloud-config
	for k, v := range conf.SSHKeyPairs {
		if strings.HasSuffix(k, "private") {
			private_keys[strings.TrimSuffix(k, "_private")] = v
		} else {
			public_keys[strings.TrimSuffix(k, "_public")] = v
		}
	}

	for key, value := range public_keys {
		c.SSHKeyPairs = append(c.SSHKeyPairs, ssh.KeyPair{
			Public:  ssh.Key(value),
			Private: ssh.Key(private_keys[key]),
		})
	}

	c.Groups = parseGroups(conf.Groups)
	c.Users = parseUsers(conf.Users)
	/*
	* BUG(yaml.v2): Embedded structs are not unmarshaled properly.
	* TODO(tmrts): Use another yaml library or extend identity.User.
	*for _, usr := range conf.Users {
	*    for _, key := range usr.AuthorizedSSHKeys {
	*       c.AuthorizedKeys[usr.Name] = append(c.AuthorizedKeys[usr.Name], ssh.Key(key))
	*    }
	*}
	 */

	c.Files = conf.Files

	return &c, nil
}

func parseCommands(runcmd []interface{}) (commands [][]string) {
	var cmds []string

	for _, cmd := range runcmd {
		switch cmd := cmd.(type) {
		case string:
			cmds = []string{"sh", "-c", cmd}
		case []interface{}:
			cmds = []string{}
			for _, s := range cmd {
				cmds = append(cmds, s.(string))
			}
		}

		commands = append(commands, cmds)
	}

	return
}

func parseUsers(users []identity.User) map[string]identity.User {
	userMap := make(map[string]identity.User)

	for _, user := range users {
		userMap[user.Name] = user
	}

	return userMap
}

func parseGroups(vals []interface{}) map[string][]string {
	groups := make(map[string][]string)

	for _, group := range vals {
		switch group := group.(type) {
		case string:
			groups[group] = []string{}
		case map[interface{}]interface{}:
			for name, users := range group {
				name := name.(string)

				members := []string{}
				for _, m := range users.([]interface{}) {
					members = append(members, m.(string))
				}

				groups[name] = members
			}
		}
	}

	return groups
}
<commit_msg>Improve cloudconfig pkg documentation<commit_after>// Package cloudconfig validates and parses a cloud-config data file.
package cloudconfig

import (
	"errors"
	"io"
	"io/ioutil"
	"strings"

	"github.com/tmrts/flamingo/pkg/sys/identity"
	"github.com/tmrts/flamingo/pkg/sys/ssh"

	"gopkg.in/yaml.v2"
)

// WriteFile represents the write_file directive found in a
// cloud-config file. It contains the fields necessary to create
// a new file in the system.
type WriteFile struct {
	Path        string
	Owner       string
	Permissions string
	Encoding    string

	Content string
}

var (
	ErrNotACloudConfigFile = errors.New("cloudconfig: not a cloud-config file")
)

// IsValid reads from the given io.ReadCloser and determines
// whether the read contents belong to a valid cloud-config file.
// It returns an ErrNotACloudConfigFile if the file is not a valid cloud-config file.
// If it encounters an error while reading from the io.ReadCloser it returns the error.
func IsValid(rdr io.ReadCloser) error {
	buf, err := ioutil.ReadAll(rdr)
	if err != nil {
		return err
	}
	rdr.Close()

	if contents := string(buf); strings.HasPrefix(contents, "#cloud-config\n") != true {
		return ErrNotACloudConfigFile
	}

	return nil
}

type user struct {
	identity.User     `yaml:""`
	AuthorizedSSHKeys []string `yaml:"ssh-authorized-keys"`
	SSHImportID       string   `yaml:"ssh-import-id"`
}

type cloudConfig struct {
	RunCMD         []interface{}
	AuthorizedKeys []string          `yaml:"ssh_authorized_keys"`
	SSHKeyPairs    map[string]string `yaml:"ssh_keys"`
	Users          []identity.User   `yaml:"users"`
	Groups         []interface{}     `yaml:"groups"`
	Files          []WriteFile       `yaml:"write_files"`
}

// Digest is the parsed cloud-config file.
type Digest struct {
	Commands       [][]string
	Files          []WriteFile
	Groups         map[string][]string
	Users          map[string]identity.User
	AuthorizedKeys map[string][]ssh.Key
	SSHKeyPairs    []ssh.KeyPair
}

// Parse reads from the given io.ReadCloser and
// parses read contents of a cloud-config file.
func Parse(rdr io.ReadCloser) (*Digest, error) {
	buf, err := ioutil.ReadAll(rdr)
	if err != nil {
		return nil, err
	}
	rdr.Close()

	var conf cloudConfig
	if err := yaml.Unmarshal(buf, &conf); err != nil {
		return nil, err
	}
	var c Digest

	c.Commands = parseCommands(conf.RunCMD)

	c.AuthorizedKeys = make(map[string][]ssh.Key)
	for _, key := range conf.AuthorizedKeys {
		c.AuthorizedKeys["root"] = append(c.AuthorizedKeys["root"], ssh.Key(key))
	}

	public_keys, private_keys := make(map[string]string), make(map[string]string)

	// TODO: Extend ssh key syntax beyond cloud-config
	for k, v := range conf.SSHKeyPairs {
		if strings.HasSuffix(k, "private") {
			private_keys[strings.TrimSuffix(k, "_private")] = v
		} else {
			public_keys[strings.TrimSuffix(k, "_public")] = v
		}
	}

	for key, value := range public_keys {
		c.SSHKeyPairs = append(c.SSHKeyPairs, ssh.KeyPair{
			Public:  ssh.Key(value),
			Private: ssh.Key(private_keys[key]),
		})
	}

	c.Groups = parseGroups(conf.Groups)
	c.Users = parseUsers(conf.Users)
	/*
	* BUG(yaml.v2): Embedded structs are not unmarshaled properly.
	* TODO(tmrts): Use another yaml library or extend identity.User.
	*for _, usr := range conf.Users {
	*    for _, key := range usr.AuthorizedSSHKeys {
	*       c.AuthorizedKeys[usr.Name] = append(c.AuthorizedKeys[usr.Name], ssh.Key(key))
	*    }
	*}
	 */

	c.Files = conf.Files

	return &c, nil
}

func parseCommands(runcmd []interface{}) (commands [][]string) {
	var cmds []string

	for _, cmd := range runcmd {
		switch cmd := cmd.(type) {
		case string:
			cmds = []string{"sh", "-c", cmd}
		case []interface{}:
			cmds = []string{}
			for _, s := range cmd {
				cmds = append(cmds, s.(string))
			}
		}

		commands = append(commands, cmds)
	}

	return
}

func parseUsers(users []identity.User) map[string]identity.User {
	userMap := make(map[string]identity.User)

	for _, user := range users {
		userMap[user.Name] = user
	}

	return userMap
}

func parseGroups(vals []interface{}) map[string][]string {
	groups := make(map[string][]string)

	for _, group := range vals {
		switch group := group.(type) {
		case string:
			groups[group] = []string{}
		case map[interface{}]interface{}:
			for name, users := range group {
				name := name.(string)

				members := []string{}
				for _, m := range users.([]interface{}) {
					members = append(members, m.(string))
				}

				groups[name] = members
			}
		}
	}

	return groups
}
<|endoftext|>


<commit_before>package main

import (
	"bytes"
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

type Executor struct {
	dir  string
	data map[string]string
}

func NewExecutor(dir string, data map[string]string) *Executor {
	return &Executor{dir, data}
}

func (this *Executor) Execute(actions ...*Action) error {
	for _, action := range actions {
		if action.Mode == "" || action.Mode == _Mode {
			var (
				err  error
				args = strings.Split(action.Args, " ")
			)

			switch action.Name {
			case "copy":
				err = this.Copy(args)
			case "replace":
				err = this.Replace(args)
			default:
				return fmt.Errorf("action [%s] is not supported", action.Name)
			}

			if err != nil {
				return fmt.Errorf("execute action error > %v", err)
			}
		}
	}

	return nil
}

func (this *Executor) Copy(args []string) error {
	if len(args) != 2 {
		return fmt.Errorf("copy action take two arguments")
	}

	src := ExpandString(args[0], this.data)
	dest := ExpandString(args[1], this.data)

	if !filepath.IsAbs(src) {
		src = filepath.Join(this.dir, src)
	}
	if !filepath.IsAbs(dest) {
		dest = filepath.Join(this.dir, dest)
	}

	if strings.ContainsAny(src, "*?") {
		return this.CopyFile(src, dest)
	}

	srcInfo, err := os.Stat(src)
	if err != nil {
		return err
	}

	if srcInfo.IsDir() {
		return this.CopyDir(src, dest)
	} else {
		return this.CopyFile(src, dest)
	}
}

func (this *Executor) CopyDir(src, dest string) error {
	srcInfo, err := os.Stat(src)

	dest = filepath.Join(dest, filepath.Base(src))
	_, err = os.Stat(dest)
	if os.IsNotExist(err) {
		err = os.MkdirAll(dest, srcInfo.Mode())
		if err != nil {
			return err
		}
	}

	return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		relatedPath := path[len(src):]
		dest := filepath.Join(dest, relatedPath)
		if info.IsDir() {
			return CreateDir(dest, info.Mode())
		} else {
			return CopyFile(path, dest, info.Mode())
		}
	})
}

func (this *Executor) CopyFile(src, dest string) error {
	// ioutil.ReadDir(dirname)
	files, err := filepath.Glob(src)
	if err != nil {
		return err
	}

	if files == nil {
		return nil
	}

	err = CreateDir(dest, os.ModePerm)
	if err != nil {
		return err
	}

	for _, f := range files {
		fi, err := os.Stat(f)
		if err != nil {
			return err
		}

		err = CopyFile(f, filepath.Join(dest, filepath.Base(f)), fi.Mode())
		if err != nil {
			return err
		}
	}
	return nil
}

func (this *Executor) Replace(args []string) error {
	var useRegexp bool
	flags := flag.NewFlagSet("action", flag.ContinueOnError)
	flags.BoolVar(&useRegexp, "r", false, "use regular expression")
	err := flags.Parse(args)
	if err != nil {
		return err
	}

	args = flags.Args()
	if len(args) != 3 {
		return fmt.Errorf("replace action should take 3 arguments")
	}

	path := ExpandString(args[0], this.data)
	if !filepath.IsAbs(path) {
		path = filepath.Join(this.dir, path)
	}
	file, err := os.Open(path)
	if err != nil {
		return err
	}
	data, err := ioutil.ReadAll(file)
	if err != nil {
		return err
	}

	if useRegexp {
		reg, err := regexp.Compile(args[1])
		if err != nil {
			return err
		}
		data = reg.ReplaceAll(data, []byte(args[2]))
	} else {
		data = bytes.Replace(data, []byte(args[1]), []byte(args[2]), -1)
	}

	fi, err := file.Stat()
	if err != nil {
		return err
	}
	return ioutil.WriteFile(path, data, fi.Mode())
}

func (this *Executor) GZip(args []string) error {
	return nil
}
<commit_msg>add support to execute system command<commit_after>package main

import (
	"bytes"
	"flag"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
)

type Executor struct {
	dir  string
	data map[string]string
}

func NewExecutor(dir string, data map[string]string) *Executor {
	return &Executor{dir, data}
}

func (this *Executor) Execute(actions ...*Action) error {
	for _, action := range actions {
		if action.Mode == "" || action.Mode == _Mode {
			var (
				err  error
				args = strings.Split(action.Args, " ")
			)

			switch action.Name {
			case "exec":
				err = this.Exec(args)
			case "copy":
				err = this.Copy(args)
			case "replace":
				err = this.Replace(args)
			default:
				return fmt.Errorf("action [%s] is not supported", action.Name)
			}

			if err != nil {
				return fmt.Errorf("execute action error > %v", err)
			}
		}
	}

	return nil
}

func (this *Executor) Exec(args []string) error {
	if len(args) == 0 {
		return fmt.Errorf("no command is specified")
	}

	for i := 1; i < len(args); i++ {
		args[i] = ExpandString(args[i], this.data)
	}

	cmd := exec.Command(args[0], args[1:]...)
	cmd.Dir = this.dir

	if result, err := cmd.CombinedOutput(); err != nil {
		return fmt.Errorf("%v, output:\n%s", err, result)
	}

	return nil
}

func (this *Executor) Copy(args []string) error {
	if len(args) != 2 {
		return fmt.Errorf("copy action take two arguments")
	}

	src := ExpandString(args[0], this.data)
	dest := ExpandString(args[1], this.data)

	if !filepath.IsAbs(src) {
		src = filepath.Join(this.dir, src)
	}
	if !filepath.IsAbs(dest) {
		dest = filepath.Join(this.dir, dest)
	}

	if strings.ContainsAny(src, "*?") {
		return this.CopyFile(src, dest)
	}

	srcInfo, err := os.Stat(src)
	if err != nil {
		return err
	}

	if srcInfo.IsDir() {
		return this.CopyDir(src, dest)
	} else {
		return this.CopyFile(src, dest)
	}
}

func (this *Executor) CopyDir(src, dest string) error {
	srcInfo, err := os.Stat(src)

	dest = filepath.Join(dest, filepath.Base(src))
	_, err = os.Stat(dest)
	if os.IsNotExist(err) {
		err = os.MkdirAll(dest, srcInfo.Mode())
		if err != nil {
			return err
		}
	}

	return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		relatedPath := path[len(src):]
		dest := filepath.Join(dest, relatedPath)
		if info.IsDir() {
			return CreateDir(dest, info.Mode())
		} else {
			return CopyFile(path, dest, info.Mode())
		}
	})
}

func (this *Executor) CopyFile(src, dest string) error {
	// ioutil.ReadDir(dirname)
	files, err := filepath.Glob(src)
	if err != nil {
		return err
	}

	if files == nil {
		return nil
	}

	err = CreateDir(dest, os.ModePerm)
	if err != nil {
		return err
	}

	for _, f := range files {
		fi, err := os.Stat(f)
		if err != nil {
			return err
		}

		err = CopyFile(f, filepath.Join(dest, filepath.Base(f)), fi.Mode())
		if err != nil {
			return err
		}
	}
	return nil
}

func (this *Executor) Replace(args []string) error {
	var useRegexp bool
	flags := flag.NewFlagSet("action", flag.ContinueOnError)
	flags.BoolVar(&useRegexp, "r", false, "use regular expression")
	err := flags.Parse(args)
	if err != nil {
		return err
	}

	args = flags.Args()
	if len(args) != 3 {
		return fmt.Errorf("replace action should take 3 arguments")
	}

	path := ExpandString(args[0], this.data)
	if !filepath.IsAbs(path) {
		path = filepath.Join(this.dir, path)
	}
	file, err := os.Open(path)
	if err != nil {
		return err
	}
	data, err := ioutil.ReadAll(file)
	if err != nil {
		return err
	}

	if useRegexp {
		reg, err := regexp.Compile(args[1])
		if err != nil {
			return err
		}
		data = reg.ReplaceAll(data, []byte(args[2]))
	} else {
		data = bytes.Replace(data, []byte(args[1]), []byte(args[2]), -1)
	}

	fi, err := file.Stat()
	if err != nil {
		return err
	}
	return ioutil.WriteFile(path, data, fi.Mode())
}

func (this *Executor) GZip(args []string) error {
	return nil
}
<|endoftext|>


<commit_before>/*
Copyright 2019 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package cpumanager

import (
	"reflect"
	"sort"
	"testing"

	cadvisorapi "github.com/google/cadvisor/info/v1"
	v1 "k8s.io/api/core/v1"
	"k8s.io/kubernetes/pkg/kubelet/cm/cpumanager/state"
	"k8s.io/kubernetes/pkg/kubelet/cm/cpumanager/topology"
	"k8s.io/kubernetes/pkg/kubelet/cm/cpuset"
	"k8s.io/kubernetes/pkg/kubelet/cm/topologymanager"
	"k8s.io/kubernetes/pkg/kubelet/cm/topologymanager/bitmask"
)

func TestGetTopologyHints(t *testing.T) {
	testPod1 := makePod("fakePod", "fakeContainer", "2", "2")
	testContainer1 := &testPod1.Spec.Containers[0]
	testPod2 := makePod("fakePod", "fakeContainer", "5", "5")
	testContainer2 := &testPod2.Spec.Containers[0]
	testPod3 := makePod("fakePod", "fakeContainer", "7", "7")
	testContainer3 := &testPod3.Spec.Containers[0]
	testPod4 := makePod("fakePod", "fakeContainer", "11", "11")
	testContainer4 := &testPod4.Spec.Containers[0]

	firstSocketMask, _ := bitmask.NewBitMask(0)
	secondSocketMask, _ := bitmask.NewBitMask(1)
	crossSocketMask, _ := bitmask.NewBitMask(0, 1)

	machineInfo := cadvisorapi.MachineInfo{
		NumCores: 12,
		Topology: []cadvisorapi.Node{
			{Id: 0,
				Cores: []cadvisorapi.Core{
					{Id: 0, Threads: []int{0, 6}},
					{Id: 1, Threads: []int{1, 7}},
					{Id: 2, Threads: []int{2, 8}},
				},
			},
			{Id: 1,
				Cores: []cadvisorapi.Core{
					{Id: 0, Threads: []int{3, 9}},
					{Id: 1, Threads: []int{4, 10}},
					{Id: 2, Threads: []int{5, 11}},
				},
			},
		},
	}

	numaNodeInfo := topology.NUMANodeInfo{
		0: cpuset.NewCPUSet(0, 6, 1, 7, 2, 8),
		1: cpuset.NewCPUSet(3, 9, 4, 10, 5, 11),
	}

	tcases := []struct {
		name          string
		pod           v1.Pod
		container     v1.Container
		assignments   state.ContainerCPUAssignments
		defaultCPUSet cpuset.CPUSet
		expectedHints []topologymanager.TopologyHint
	}{
		{
			name:          "Request 2 CPUs, 4 available on NUMA 0, 6 available on NUMA 1",
			pod:           *testPod1,
			container:     *testContainer1,
			defaultCPUSet: cpuset.NewCPUSet(2, 3, 4, 5, 6, 7, 8, 9, 10, 11),
			expectedHints: []topologymanager.TopologyHint{
				{
					NUMANodeAffinity: firstSocketMask,
					Preferred:        true,
				},
				{
					NUMANodeAffinity: secondSocketMask,
					Preferred:        true,
				},
				{
					NUMANodeAffinity: crossSocketMask,
					Preferred:        false,
				},
			},
		},
		{
			name:          "Request 5 CPUs, 4 available on NUMA 0, 6 available on NUMA 1",
			pod:           *testPod2,
			container:     *testContainer2,
			defaultCPUSet: cpuset.NewCPUSet(2, 3, 4, 5, 6, 7, 8, 9, 10, 11),
			expectedHints: []topologymanager.TopologyHint{
				{
					NUMANodeAffinity: secondSocketMask,
					Preferred:        true,
				},
				{
					NUMANodeAffinity: crossSocketMask,
					Preferred:        false,
				},
			},
		},
		{
			name:          "Request 7 CPUs, 4 available on NUMA 0, 6 available on NUMA 1",
			pod:           *testPod3,
			container:     *testContainer3,
			defaultCPUSet: cpuset.NewCPUSet(2, 3, 4, 5, 6, 7, 8, 9, 10, 11),
			expectedHints: []topologymanager.TopologyHint{
				{
					NUMANodeAffinity: crossSocketMask,
					Preferred:        true,
				},
			},
		},
		{
			name:          "Request 11 CPUs, 4 available on NUMA 0, 6 available on NUMA 1",
			pod:           *testPod4,
			container:     *testContainer4,
			defaultCPUSet: cpuset.NewCPUSet(2, 3, 4, 5, 6, 7, 8, 9, 10, 11),
			expectedHints: nil,
		},
		{
			name:          "Request 2 CPUs, 1 available on NUMA 0, 1 available on NUMA 1",
			pod:           *testPod1,
			container:     *testContainer1,
			defaultCPUSet: cpuset.NewCPUSet(0, 3),
			expectedHints: []topologymanager.TopologyHint{
				{
					NUMANodeAffinity: crossSocketMask,
					Preferred:        false,
				},
			},
		},
		{
			name:          "Request more CPUs than available",
			pod:           *testPod2,
			container:     *testContainer2,
			defaultCPUSet: cpuset.NewCPUSet(0, 1, 2, 3),
			expectedHints: nil,
		},
		{
			name:      "Regenerate Single-Node NUMA Hints if already allocated 1/2",
			pod:       *testPod1,
			container: *testContainer1,
			assignments: state.ContainerCPUAssignments{
				string(testPod1.UID): map[string]cpuset.CPUSet{
					testContainer1.Name: cpuset.NewCPUSet(0, 6),
				},
			},
			defaultCPUSet: cpuset.NewCPUSet(),
			expectedHints: []topologymanager.TopologyHint{
				{
					NUMANodeAffinity: firstSocketMask,
					Preferred:        true,
				},
				{
					NUMANodeAffinity: crossSocketMask,
					Preferred:        false,
				},
			},
		},
		{
			name:      "Regenerate Single-Node NUMA Hints if already allocated 1/2",
			pod:       *testPod1,
			container: *testContainer1,
			assignments: state.ContainerCPUAssignments{
				string(testPod1.UID): map[string]cpuset.CPUSet{
					testContainer1.Name: cpuset.NewCPUSet(3, 9),
				},
			},
			defaultCPUSet: cpuset.NewCPUSet(),
			expectedHints: []topologymanager.TopologyHint{
				{
					NUMANodeAffinity: secondSocketMask,
					Preferred:        true,
				},
				{
					NUMANodeAffinity: crossSocketMask,
					Preferred:        false,
				},
			},
		},
		{
			name:      "Regenerate Cross-NUMA Hints if already allocated",
			pod:       *testPod4,
			container: *testContainer4,
			assignments: state.ContainerCPUAssignments{
				string(testPod4.UID): map[string]cpuset.CPUSet{
					testContainer4.Name: cpuset.NewCPUSet(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
				},
			},
			defaultCPUSet: cpuset.NewCPUSet(),
			expectedHints: []topologymanager.TopologyHint{
				{
					NUMANodeAffinity: crossSocketMask,
					Preferred:        true,
				},
			},
		},
		{
			name:      "Requested less than already allocated",
			pod:       *testPod1,
			container: *testContainer1,
			assignments: state.ContainerCPUAssignments{
				string(testPod1.UID): map[string]cpuset.CPUSet{
					testContainer1.Name: cpuset.NewCPUSet(0, 6, 3, 9),
				},
			},
			defaultCPUSet: cpuset.NewCPUSet(),
			expectedHints: []topologymanager.TopologyHint{},
		},
		{
			name:      "Requested more than already allocated",
			pod:       *testPod4,
			container: *testContainer4,
			assignments: state.ContainerCPUAssignments{
				string(testPod4.UID): map[string]cpuset.CPUSet{
					testContainer4.Name: cpuset.NewCPUSet(0, 6, 3, 9),
				},
			},
			defaultCPUSet: cpuset.NewCPUSet(),
			expectedHints: []topologymanager.TopologyHint{},
		},
	}
	for _, tc := range tcases {
		topology, _ := topology.Discover(&machineInfo, numaNodeInfo)

		m := manager{
			policy: &staticPolicy{
				topology: topology,
			},
			state: &mockState{
				assignments:   tc.assignments,
				defaultCPUSet: tc.defaultCPUSet,
			},
			topology:          topology,
			activePods:        func() []*v1.Pod { return nil },
			podStatusProvider: mockPodStatusProvider{},
			sourcesReady:      &sourcesReadyStub{},
		}

		hints := m.GetTopologyHints(tc.pod, tc.container)[string(v1.ResourceCPU)]
		if len(tc.expectedHints) == 0 && len(hints) == 0 {
			continue
		}
		sort.SliceStable(hints, func(i, j int) bool {
			return hints[i].LessThan(hints[j])
		})
		sort.SliceStable(tc.expectedHints, func(i, j int) bool {
			return tc.expectedHints[i].LessThan(tc.expectedHints[j])
		})
		if !reflect.DeepEqual(tc.expectedHints, hints) {
			t.Errorf("Expected in result to be %v , got %v", tc.expectedHints, hints)
		}
	}
}
<commit_msg>Add proper activePods list in TestGetTopologyHints for CPUManager<commit_after>/*
Copyright 2019 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package cpumanager

import (
	"reflect"
	"sort"
	"testing"

	cadvisorapi "github.com/google/cadvisor/info/v1"
	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/kubernetes/pkg/kubelet/cm/cpumanager/state"
	"k8s.io/kubernetes/pkg/kubelet/cm/cpumanager/topology"
	"k8s.io/kubernetes/pkg/kubelet/cm/cpuset"
	"k8s.io/kubernetes/pkg/kubelet/cm/topologymanager"
	"k8s.io/kubernetes/pkg/kubelet/cm/topologymanager/bitmask"
)

func TestGetTopologyHints(t *testing.T) {
	testPod1 := makePod("fakePod", "fakeContainer", "2", "2")
	testContainer1 := &testPod1.Spec.Containers[0]
	testPod2 := makePod("fakePod", "fakeContainer", "5", "5")
	testContainer2 := &testPod2.Spec.Containers[0]
	testPod3 := makePod("fakePod", "fakeContainer", "7", "7")
	testContainer3 := &testPod3.Spec.Containers[0]
	testPod4 := makePod("fakePod", "fakeContainer", "11", "11")
	testContainer4 := &testPod4.Spec.Containers[0]

	firstSocketMask, _ := bitmask.NewBitMask(0)
	secondSocketMask, _ := bitmask.NewBitMask(1)
	crossSocketMask, _ := bitmask.NewBitMask(0, 1)

	machineInfo := cadvisorapi.MachineInfo{
		NumCores: 12,
		Topology: []cadvisorapi.Node{
			{Id: 0,
				Cores: []cadvisorapi.Core{
					{Id: 0, Threads: []int{0, 6}},
					{Id: 1, Threads: []int{1, 7}},
					{Id: 2, Threads: []int{2, 8}},
				},
			},
			{Id: 1,
				Cores: []cadvisorapi.Core{
					{Id: 0, Threads: []int{3, 9}},
					{Id: 1, Threads: []int{4, 10}},
					{Id: 2, Threads: []int{5, 11}},
				},
			},
		},
	}

	numaNodeInfo := topology.NUMANodeInfo{
		0: cpuset.NewCPUSet(0, 6, 1, 7, 2, 8),
		1: cpuset.NewCPUSet(3, 9, 4, 10, 5, 11),
	}

	tcases := []struct {
		name          string
		pod           v1.Pod
		container     v1.Container
		assignments   state.ContainerCPUAssignments
		defaultCPUSet cpuset.CPUSet
		expectedHints []topologymanager.TopologyHint
	}{
		{
			name:          "Request 2 CPUs, 4 available on NUMA 0, 6 available on NUMA 1",
			pod:           *testPod1,
			container:     *testContainer1,
			defaultCPUSet: cpuset.NewCPUSet(2, 3, 4, 5, 6, 7, 8, 9, 10, 11),
			expectedHints: []topologymanager.TopologyHint{
				{
					NUMANodeAffinity: firstSocketMask,
					Preferred:        true,
				},
				{
					NUMANodeAffinity: secondSocketMask,
					Preferred:        true,
				},
				{
					NUMANodeAffinity: crossSocketMask,
					Preferred:        false,
				},
			},
		},
		{
			name:          "Request 5 CPUs, 4 available on NUMA 0, 6 available on NUMA 1",
			pod:           *testPod2,
			container:     *testContainer2,
			defaultCPUSet: cpuset.NewCPUSet(2, 3, 4, 5, 6, 7, 8, 9, 10, 11),
			expectedHints: []topologymanager.TopologyHint{
				{
					NUMANodeAffinity: secondSocketMask,
					Preferred:        true,
				},
				{
					NUMANodeAffinity: crossSocketMask,
					Preferred:        false,
				},
			},
		},
		{
			name:          "Request 7 CPUs, 4 available on NUMA 0, 6 available on NUMA 1",
			pod:           *testPod3,
			container:     *testContainer3,
			defaultCPUSet: cpuset.NewCPUSet(2, 3, 4, 5, 6, 7, 8, 9, 10, 11),
			expectedHints: []topologymanager.TopologyHint{
				{
					NUMANodeAffinity: crossSocketMask,
					Preferred:        true,
				},
			},
		},
		{
			name:          "Request 11 CPUs, 4 available on NUMA 0, 6 available on NUMA 1",
			pod:           *testPod4,
			container:     *testContainer4,
			defaultCPUSet: cpuset.NewCPUSet(2, 3, 4, 5, 6, 7, 8, 9, 10, 11),
			expectedHints: nil,
		},
		{
			name:          "Request 2 CPUs, 1 available on NUMA 0, 1 available on NUMA 1",
			pod:           *testPod1,
			container:     *testContainer1,
			defaultCPUSet: cpuset.NewCPUSet(0, 3),
			expectedHints: []topologymanager.TopologyHint{
				{
					NUMANodeAffinity: crossSocketMask,
					Preferred:        false,
				},
			},
		},
		{
			name:          "Request more CPUs than available",
			pod:           *testPod2,
			container:     *testContainer2,
			defaultCPUSet: cpuset.NewCPUSet(0, 1, 2, 3),
			expectedHints: nil,
		},
		{
			name:      "Regenerate Single-Node NUMA Hints if already allocated 1/2",
			pod:       *testPod1,
			container: *testContainer1,
			assignments: state.ContainerCPUAssignments{
				string(testPod1.UID): map[string]cpuset.CPUSet{
					testContainer1.Name: cpuset.NewCPUSet(0, 6),
				},
			},
			defaultCPUSet: cpuset.NewCPUSet(),
			expectedHints: []topologymanager.TopologyHint{
				{
					NUMANodeAffinity: firstSocketMask,
					Preferred:        true,
				},
				{
					NUMANodeAffinity: crossSocketMask,
					Preferred:        false,
				},
			},
		},
		{
			name:      "Regenerate Single-Node NUMA Hints if already allocated 1/2",
			pod:       *testPod1,
			container: *testContainer1,
			assignments: state.ContainerCPUAssignments{
				string(testPod1.UID): map[string]cpuset.CPUSet{
					testContainer1.Name: cpuset.NewCPUSet(3, 9),
				},
			},
			defaultCPUSet: cpuset.NewCPUSet(),
			expectedHints: []topologymanager.TopologyHint{
				{
					NUMANodeAffinity: secondSocketMask,
					Preferred:        true,
				},
				{
					NUMANodeAffinity: crossSocketMask,
					Preferred:        false,
				},
			},
		},
		{
			name:      "Regenerate Cross-NUMA Hints if already allocated",
			pod:       *testPod4,
			container: *testContainer4,
			assignments: state.ContainerCPUAssignments{
				string(testPod4.UID): map[string]cpuset.CPUSet{
					testContainer4.Name: cpuset.NewCPUSet(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
				},
			},
			defaultCPUSet: cpuset.NewCPUSet(),
			expectedHints: []topologymanager.TopologyHint{
				{
					NUMANodeAffinity: crossSocketMask,
					Preferred:        true,
				},
			},
		},
		{
			name:      "Requested less than already allocated",
			pod:       *testPod1,
			container: *testContainer1,
			assignments: state.ContainerCPUAssignments{
				string(testPod1.UID): map[string]cpuset.CPUSet{
					testContainer1.Name: cpuset.NewCPUSet(0, 6, 3, 9),
				},
			},
			defaultCPUSet: cpuset.NewCPUSet(),
			expectedHints: []topologymanager.TopologyHint{},
		},
		{
			name:      "Requested more than already allocated",
			pod:       *testPod4,
			container: *testContainer4,
			assignments: state.ContainerCPUAssignments{
				string(testPod4.UID): map[string]cpuset.CPUSet{
					testContainer4.Name: cpuset.NewCPUSet(0, 6, 3, 9),
				},
			},
			defaultCPUSet: cpuset.NewCPUSet(),
			expectedHints: []topologymanager.TopologyHint{},
		},
	}
	for _, tc := range tcases {
		topology, _ := topology.Discover(&machineInfo, numaNodeInfo)

		var activePods []*v1.Pod
		for p := range tc.assignments {
			pod := v1.Pod{}
			pod.UID = types.UID(p)
			for c := range tc.assignments[p] {
				container := v1.Container{}
				container.Name = c
				pod.Spec.Containers = append(pod.Spec.Containers, container)
			}
			activePods = append(activePods, &pod)
		}

		m := manager{
			policy: &staticPolicy{
				topology: topology,
			},
			state: &mockState{
				assignments:   tc.assignments,
				defaultCPUSet: tc.defaultCPUSet,
			},
			topology:          topology,
			activePods:        func() []*v1.Pod { return activePods },
			podStatusProvider: mockPodStatusProvider{},
			sourcesReady:      &sourcesReadyStub{},
		}

		hints := m.GetTopologyHints(tc.pod, tc.container)[string(v1.ResourceCPU)]
		if len(tc.expectedHints) == 0 && len(hints) == 0 {
			continue
		}
		sort.SliceStable(hints, func(i, j int) bool {
			return hints[i].LessThan(hints[j])
		})
		sort.SliceStable(tc.expectedHints, func(i, j int) bool {
			return tc.expectedHints[i].LessThan(tc.expectedHints[j])
		})
		if !reflect.DeepEqual(tc.expectedHints, hints) {
			t.Errorf("Expected in result to be %v , got %v", tc.expectedHints, hints)
		}
	}
}
<|endoftext|>


<commit_before>package main

//FilterChannel cuts off unneeded images
func FilterChannel(in ImageCh) (out ImageCh) {

	if opts.Filter {
		return in
	}
	out = make(ImageCh)
	go func() {
		for imgdata := range in {

			if imgdata.Score >= opts.Score {
				out <- imgdata
				continue
			}
			elog.Println("Filtering " + imgdata.Filename)
		}
		close(out)
	}()
	return out
}
<commit_msg>Logging in filter crashes the program. No idea why<commit_after>package main

//FilterChannel cuts off unneeded images
func FilterChannel(in ImageCh) (out ImageCh) {
	//elog.Println("filtering")
	if !opts.Filter {
		//elog.Println("Filter is off")
		return in
	}
	//elog.Println("Filter is on")
	out = make(ImageCh, 1)
	go func() {
		for imgdata := range in {

			if imgdata.Score >= opts.Score {
				out <- imgdata
				continue
			}
			//elog.Println("Filtering " + imgdata.Filename)
		}
		close(out)
	}()
	return out
}
<|endoftext|>