<commit_before>// Copyright 2020 Google LLC
//
// 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 modeler implements periodic statistical calculations.
package modeler

import (
	"context"
	"fmt"
	"math"
	"net/http"
	"time"

	"github.com/gonum/matrix/mat64"
	"github.com/google/exposure-notifications-server/pkg/logging"
	"github.com/google/exposure-notifications-verification-server/pkg/config"
	"github.com/google/exposure-notifications-verification-server/pkg/database"
	"github.com/google/exposure-notifications-verification-server/pkg/render"
	"github.com/hashicorp/go-multierror"
	"go.opencensus.io/stats"

	"github.com/sethvargo/go-limiter"
)

const modelerLock = "modelerLock"

// Controller is a controller for the modeler service.
type Controller struct {
	config  *config.Modeler
	db      *database.Database
	h       *render.Renderer
	limiter limiter.Store
}

// New creates a new modeler controller.
func New(ctx context.Context, config *config.Modeler, db *database.Database, limiter limiter.Store, h *render.Renderer) *Controller {
	return &Controller{
		config:  config,
		db:      db,
		h:       h,
		limiter: limiter,
	}
}

// HandleModel accepts an HTTP trigger and re-generates the models.
func (c *Controller) HandleModel() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		logger := logging.FromContext(ctx).Named("modeler.HandleModel")
		logger.Debugw("starting")
		defer logger.Debugw("finishing")

		ok, err := c.db.TryLock(ctx, modelerLock, 15*time.Minute)
		if err != nil {
			logger.Errorw("failed to acquire lock", "error", err)
			c.h.RenderJSON(w, http.StatusInternalServerError, err)
			return
		}
		if !ok {
			logger.Debugw("skipping (too early)")
			c.h.RenderJSON(w, http.StatusOK, fmt.Errorf("too early"))
			return
		}

		if err := c.rebuildModels(ctx); err != nil {
			logger.Errorw("failed to build models", "error", err)
			c.h.RenderJSON500(w, err)
			return
		}

		if merr := c.rebuildModels(ctx); merr != nil {
			if errs := merr.WrappedErrors(); len(errs) > 0 {
				logger.Errorw("failed to rebuild models", "errors", errs)
				c.h.RenderJSON(w, http.StatusInternalServerError, errs)
				return
			}
		}

		stats.Record(ctx, mSuccess.M(1))
		c.h.RenderJSON(w, http.StatusOK, nil)
	})
}

// rebuildModels iterates over all models with abuse prevention enabled,
// calculates the new limits, and updates the new limits.
func (c *Controller) rebuildModels(ctx context.Context) *multierror.Error {
	logger := logging.FromContext(ctx).Named("modeler.rebuildModels")

	var merr *multierror.Error

	// Get all realm IDs in a single operation so we can iterate realm-by-realm to
	// avoid a full table lock during stats calculation.
	ids, err := c.db.AbusePreventionEnabledRealmIDs()
	if err != nil {
		merr = multierror.Append(merr, fmt.Errorf("failed to fetch ids: %w", err))
		return merr
	}
	logger.Debugw("building models", "count", len(ids))

	// Process all models.
	for _, id := range ids {
		if err := c.rebuildModel(ctx, id); err != nil {
			merr = multierror.Append(merr, fmt.Errorf("failed to update realm %d: %w", id, err))
		}
	}

	return merr
}

// rebuildModel rebuilds and updates the model for a single model.
func (c *Controller) rebuildModel(ctx context.Context, id uint64) error {
	logger := logging.FromContext(ctx).Named("modeler.rebuildModel").With("id", id)

	// Lookup the realm.
	realm, err := c.db.FindRealm(id)
	if err != nil {
		return fmt.Errorf("failed to find realm: %w", err)
	}

	// Get 21 days of historical data for the realm.
	stats, err := realm.HistoricalCodesIssued(c.db, 21)
	if err != nil {
		return fmt.Errorf("failed to get stats: %w", err)
	}

	// Require some reasonable number of days of history before attempting to
	// build a model.
	if l := len(stats); l < 14 {
		logger.Warnw("skipping, not enough data", "points", l)
		return nil
	}

	// Exclude the most recent record. Depending on timezones, the "day" might not
	// be over at 00:00 UTC, and we don't want to generate a partial model.
	stats = stats[:len(stats)-1]

	// Reverse the list - it came in reversed because we sorted by date DESC, but
	// the model expects the date to be in ascending order.
	for i, j := 0, len(stats)-1; i < j; i, j = i+1, j-1 {
		stats[i], stats[j] = stats[j], stats[i]
	}

	// Build the list of Xs and Ys.
	xs := make([]float64, len(stats))
	ys := make([]float64, len(stats))
	for i, v := range stats {
		xs[i] = float64(i)
		ys[i] = float64(v)
	}

	// This is probably overkill, but it enables us to pick a different curve in
	// the future, if we want.
	degree := 1
	alpha := vandermonde(xs, degree)
	beta := mat64.NewDense(len(ys), 1, ys)
	gamma := mat64.NewDense(degree+1, 1, nil)
	qr := new(mat64.QR)
	qr.Factorize(alpha)
	if err := gamma.SolveQR(qr, false, beta); err != nil {
		return fmt.Errorf("failed to solve QR: %w", err)
	}

	// Build the curve function.
	m := gamma.RawMatrix()
	curve := func(x float64) float64 {
		var result float64
		for i := len(m.Data) - 1; i >= 0; i-- {
			result += m.Data[i] * math.Pow(x, float64(i))
		}
		return result
	}

	// In the case of a sharp decline, the model might predict a very low value,
	// potentially less than zero. We need to do the negative check against the
	// float value before casting to a uint, or else risk overflowing if this
	// value is negative.
	nextFloat := math.Ceil(curve(float64(len(ys))))
	if nextFloat < 0 {
		nextFloat = 0
	}

	// Calculate the predicted next value as a uint.
	next := uint(nextFloat)

	// This should really never happen - it means there's been a very sharp
	// decline in the number of codes issued. In that case, we want to revert
	// back to the default minimum.
	if next < c.config.MinValue {
		logger.Debugw("next is less than min, using min", "next", next, "min", c.config.MinValue)
		next = c.config.MinValue
	}

	// Ensure we don't exceed the number at which the math gods get angry.
	if next > c.config.MaxValue {
		logger.Debugw("next is greater than allowed max, using max", "next", next, "max", c.config.MaxValue)
		next = c.config.MaxValue
	}

	logger.Debugw("next value", "value", next)

	// Save the new value back, bypassing any validation.
	realm.AbusePreventionLimit = next
	if err := c.db.SaveRealm(realm, database.System); err != nil {
		return fmt.Errorf("failed to save model: %w", err)
	}

	// Calculate effective limit.
	effective := realm.AbusePreventionEffectiveLimit()

	logger.Debugw("next effective limit", "value", effective)

	// Update the limiter to use the new value.
	key, err := realm.QuotaKey(c.config.RateLimit.HMACKey)
	if err != nil {
		return fmt.Errorf("failed to digest realm id: %w", err)
	}
	if err := c.limiter.Set(ctx, key, uint64(effective), 24*time.Hour); err != nil {
		return fmt.Errorf("failed to update limit: %w", err)
	}

	return nil
}

// vandermonde creates a Vandermonde projection (matrix) of the given degree.
func vandermonde(a []float64, degree int) *mat64.Dense {
	x := mat64.NewDense(len(a), degree+1, nil)
	for i := range a {
		for j, p := 0, 1.; j <= degree; j, p = j+1, p*a[i] {
			x.Set(i, j, p)
		}
	}
	return x
}
<commit_msg>Return validation error messages in modeler (#2161)<commit_after>// Copyright 2020 Google LLC
//
// 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 modeler implements periodic statistical calculations.
package modeler

import (
	"context"
	"fmt"
	"math"
	"net/http"
	"time"

	"github.com/gonum/matrix/mat64"
	"github.com/google/exposure-notifications-server/pkg/logging"
	"github.com/google/exposure-notifications-verification-server/pkg/config"
	"github.com/google/exposure-notifications-verification-server/pkg/database"
	"github.com/google/exposure-notifications-verification-server/pkg/render"
	"github.com/hashicorp/go-multierror"
	"go.opencensus.io/stats"

	"github.com/sethvargo/go-limiter"
)

const modelerLock = "modelerLock"

// Controller is a controller for the modeler service.
type Controller struct {
	config  *config.Modeler
	db      *database.Database
	h       *render.Renderer
	limiter limiter.Store
}

// New creates a new modeler controller.
func New(ctx context.Context, config *config.Modeler, db *database.Database, limiter limiter.Store, h *render.Renderer) *Controller {
	return &Controller{
		config:  config,
		db:      db,
		h:       h,
		limiter: limiter,
	}
}

// HandleModel accepts an HTTP trigger and re-generates the models.
func (c *Controller) HandleModel() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()

		logger := logging.FromContext(ctx).Named("modeler.HandleModel")
		logger.Debugw("starting")
		defer logger.Debugw("finishing")

		ok, err := c.db.TryLock(ctx, modelerLock, 15*time.Minute)
		if err != nil {
			logger.Errorw("failed to acquire lock", "error", err)
			c.h.RenderJSON(w, http.StatusInternalServerError, err)
			return
		}
		if !ok {
			logger.Debugw("skipping (too early)")
			c.h.RenderJSON(w, http.StatusOK, fmt.Errorf("too early"))
			return
		}

		if err := c.rebuildModels(ctx); err != nil {
			logger.Errorw("failed to build models", "error", err)
			c.h.RenderJSON500(w, err)
			return
		}

		if merr := c.rebuildModels(ctx); merr != nil {
			if errs := merr.WrappedErrors(); len(errs) > 0 {
				logger.Errorw("failed to rebuild models", "errors", errs)
				c.h.RenderJSON(w, http.StatusInternalServerError, errs)
				return
			}
		}

		stats.Record(ctx, mSuccess.M(1))
		c.h.RenderJSON(w, http.StatusOK, nil)
	})
}

// rebuildModels iterates over all models with abuse prevention enabled,
// calculates the new limits, and updates the new limits.
func (c *Controller) rebuildModels(ctx context.Context) *multierror.Error {
	logger := logging.FromContext(ctx).Named("modeler.rebuildModels")

	var merr *multierror.Error

	// Get all realm IDs in a single operation so we can iterate realm-by-realm to
	// avoid a full table lock during stats calculation.
	ids, err := c.db.AbusePreventionEnabledRealmIDs()
	if err != nil {
		merr = multierror.Append(merr, fmt.Errorf("failed to fetch ids: %w", err))
		return merr
	}
	logger.Debugw("building models", "count", len(ids))

	// Process all models.
	for _, id := range ids {
		if err := c.rebuildModel(ctx, id); err != nil {
			merr = multierror.Append(merr, fmt.Errorf("failed to update realm %d: %w", id, err))
		}
	}

	return merr
}

// rebuildModel rebuilds and updates the model for a single model.
func (c *Controller) rebuildModel(ctx context.Context, id uint64) error {
	logger := logging.FromContext(ctx).Named("modeler.rebuildModel").With("id", id)

	// Lookup the realm.
	realm, err := c.db.FindRealm(id)
	if err != nil {
		return fmt.Errorf("failed to find realm: %w", err)
	}

	// Get 21 days of historical data for the realm.
	stats, err := realm.HistoricalCodesIssued(c.db, 21)
	if err != nil {
		return fmt.Errorf("failed to get stats: %w", err)
	}

	// Require some reasonable number of days of history before attempting to
	// build a model.
	if l := len(stats); l < 14 {
		logger.Warnw("skipping, not enough data", "points", l)
		return nil
	}

	// Exclude the most recent record. Depending on timezones, the "day" might not
	// be over at 00:00 UTC, and we don't want to generate a partial model.
	stats = stats[:len(stats)-1]

	// Reverse the list - it came in reversed because we sorted by date DESC, but
	// the model expects the date to be in ascending order.
	for i, j := 0, len(stats)-1; i < j; i, j = i+1, j-1 {
		stats[i], stats[j] = stats[j], stats[i]
	}

	// Build the list of Xs and Ys.
	xs := make([]float64, len(stats))
	ys := make([]float64, len(stats))
	for i, v := range stats {
		xs[i] = float64(i)
		ys[i] = float64(v)
	}

	// This is probably overkill, but it enables us to pick a different curve in
	// the future, if we want.
	degree := 1
	alpha := vandermonde(xs, degree)
	beta := mat64.NewDense(len(ys), 1, ys)
	gamma := mat64.NewDense(degree+1, 1, nil)
	qr := new(mat64.QR)
	qr.Factorize(alpha)
	if err := gamma.SolveQR(qr, false, beta); err != nil {
		return fmt.Errorf("failed to solve QR: %w", err)
	}

	// Build the curve function.
	m := gamma.RawMatrix()
	curve := func(x float64) float64 {
		var result float64
		for i := len(m.Data) - 1; i >= 0; i-- {
			result += m.Data[i] * math.Pow(x, float64(i))
		}
		return result
	}

	// In the case of a sharp decline, the model might predict a very low value,
	// potentially less than zero. We need to do the negative check against the
	// float value before casting to a uint, or else risk overflowing if this
	// value is negative.
	nextFloat := math.Ceil(curve(float64(len(ys))))
	if nextFloat < 0 {
		nextFloat = 0
	}

	// Calculate the predicted next value as a uint.
	next := uint(nextFloat)

	// This should really never happen - it means there's been a very sharp
	// decline in the number of codes issued. In that case, we want to revert
	// back to the default minimum.
	if next < c.config.MinValue {
		logger.Debugw("next is less than min, using min", "next", next, "min", c.config.MinValue)
		next = c.config.MinValue
	}

	// Ensure we don't exceed the number at which the math gods get angry.
	if next > c.config.MaxValue {
		logger.Debugw("next is greater than allowed max, using max", "next", next, "max", c.config.MaxValue)
		next = c.config.MaxValue
	}

	logger.Debugw("next value", "value", next)

	// Save the new value back, bypassing any validation.
	realm.AbusePreventionLimit = next
	if err := c.db.SaveRealm(realm, database.System); err != nil {
		return fmt.Errorf("failed to save model: %w, errors: %q", err, realm.ErrorMessages())
	}

	// Calculate effective limit.
	effective := realm.AbusePreventionEffectiveLimit()

	logger.Debugw("next effective limit", "value", effective)

	// Update the limiter to use the new value.
	key, err := realm.QuotaKey(c.config.RateLimit.HMACKey)
	if err != nil {
		return fmt.Errorf("failed to digest realm id: %w", err)
	}
	if err := c.limiter.Set(ctx, key, uint64(effective), 24*time.Hour); err != nil {
		return fmt.Errorf("failed to update limit: %w", err)
	}

	return nil
}

// vandermonde creates a Vandermonde projection (matrix) of the given degree.
func vandermonde(a []float64, degree int) *mat64.Dense {
	x := mat64.NewDense(len(a), degree+1, nil)
	for i := range a {
		for j, p := 0, 1.; j <= degree; j, p = j+1, p*a[i] {
			x.Set(i, j, p)
		}
	}
	return x
}
<|endoftext|>


<commit_before>// Copyright © 2019 The Knative 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 commands

import (
	"errors"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	servingv1alpha1 "github.com/knative/serving/pkg/apis/serving/v1alpha1"

	serving_lib "github.com/knative/client/pkg/serving"
	"github.com/spf13/cobra"
)

func NewServiceCreateCommand(p *KnParams) *cobra.Command {
	var editFlags ConfigurationEditFlags

	serviceCreateCommand := &cobra.Command{
		Use:   "create NAME --image IMAGE",
		Short: "Create a service.",
		RunE: func(cmd *cobra.Command, args []string) (err error) {
			if len(args) != 1 {
				return errors.New("requires the service name.")
			}
			if editFlags.Image == "" {
				return errors.New("requires the image name to run.")
			}

			namespace := cmd.Flag("namespace").Value.String()

			service := servingv1alpha1.Service{
				ObjectMeta: metav1.ObjectMeta{
					Name:      args[0],
					Namespace: namespace,
				},
			}
			service.Spec.RunLatest = &servingv1alpha1.RunLatestType{}

			config, err := serving_lib.GetConfiguration(&service)
			if err != nil {
				return err
			}
			err = editFlags.Apply(config)
			if err != nil {
				return err
			}
			client, err := p.ServingFactory()
			if err != nil {
				return err
			}
			_, err = client.Services(namespace).Create(&service)
			if err != nil {
				return err
			}

			return nil
		},
	}
	editFlags.AddFlags(serviceCreateCommand)
	return serviceCreateCommand
}
<commit_msg>Adds example usage in service create command (#62)<commit_after>// Copyright © 2019 The Knative 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 commands

import (
	"errors"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	servingv1alpha1 "github.com/knative/serving/pkg/apis/serving/v1alpha1"

	serving_lib "github.com/knative/client/pkg/serving"
	"github.com/spf13/cobra"
)

func NewServiceCreateCommand(p *KnParams) *cobra.Command {
	var editFlags ConfigurationEditFlags

	serviceCreateCommand := &cobra.Command{
		Use:   "create NAME --image IMAGE",
		Short: "Create a service.",
		Example: `
  # Create a service 'mysvc' using image at dev.local/ns/image:latest
  kn service create mysvc --image dev.local/ns/image:latest

  # Create a service with multiple environment variables
  kn service create mysvc --env KEY1=VALUE1 --env KEY2=VALUE2 --image dev.local/ns/image:latest`,

		RunE: func(cmd *cobra.Command, args []string) (err error) {
			if len(args) != 1 {
				return errors.New("requires the service name.")
			}
			if editFlags.Image == "" {
				return errors.New("requires the image name to run.")
			}

			namespace := cmd.Flag("namespace").Value.String()

			service := servingv1alpha1.Service{
				ObjectMeta: metav1.ObjectMeta{
					Name:      args[0],
					Namespace: namespace,
				},
			}
			service.Spec.RunLatest = &servingv1alpha1.RunLatestType{}

			config, err := serving_lib.GetConfiguration(&service)
			if err != nil {
				return err
			}
			err = editFlags.Apply(config)
			if err != nil {
				return err
			}
			client, err := p.ServingFactory()
			if err != nil {
				return err
			}
			_, err = client.Services(namespace).Create(&service)
			if err != nil {
				return err
			}

			return nil
		},
	}
	editFlags.AddFlags(serviceCreateCommand)
	return serviceCreateCommand
}
<|endoftext|>


<commit_before>/*
Copyright 2018 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 loadbalancers

import (
	"crypto/sha256"
	"fmt"
	"strings"

	"github.com/golang/glog"
	compute "google.golang.org/api/compute/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/selection"
	"k8s.io/ingress-gce/pkg/flags"
	"k8s.io/ingress-gce/pkg/utils"
)

const SslCertificateMissing = "SslCertificateMissing"

func (l *L7) checkSSLCert() error {
	if flags.F.Features.ManagedCertificates {
		// Handle annotation managed-certificates
		managedSslCerts, used, err := l.getManagedCertificates()
		if used {
			l.sslCerts = managedSslCerts
			return err
		}
	}

	// Handle annotation pre-shared-cert
	used, preSharedSslCerts, err := l.getPreSharedCertificates()
	if used {
		l.sslCerts = preSharedSslCerts
		return err
	}

	// Get updated value of certificate for comparison
	existingSecretsSslCerts, err := l.getIngressManagedSslCerts()
	if err != nil {
		return err
	}

	l.oldSSLCerts = existingSecretsSslCerts
	secretsSslCerts, err := l.createSecretsSslCertificates(existingSecretsSslCerts)
	l.sslCerts = secretsSslCerts
	return err
}

// createSecretsSslCertificates creates SslCertificates based on kubernetes secrets in Ingress configuration.
func (l *L7) createSecretsSslCertificates(existingCerts []*compute.SslCertificate) ([]*compute.SslCertificate, error) {
	var result []*compute.SslCertificate

	existingCertsMap := getMapfromCertList(existingCerts)

	// mapping of currently configured certs
	visitedCertMap := make(map[string]string)
	var failedCerts []string

	for _, tlsCert := range l.runtimeInfo.TLS {
		ingCert := tlsCert.Cert
		ingKey := tlsCert.Key
		gcpCertName := l.namer.SSLCertName(l.Name, tlsCert.CertHash)

		if addedBy, exists := visitedCertMap[gcpCertName]; exists {
			glog.V(3).Infof("Secret %q has a certificate already used by %v", tlsCert.Name, addedBy)
			continue
		}

		// PrivateKey is write only, so compare certs alone. We're assuming that
		// no one will change just the key. We can remember the key and compare,
		// but a bug could end up leaking it, which feels worse.
		// If the cert contents have changed, its hash would be different, so would be the cert name. So it is enough
		// to check if this cert name exists in the map.
		if existingCertsMap != nil {
			if cert, ok := existingCertsMap[gcpCertName]; ok {
				glog.V(3).Infof("Secret %q already exists as certificate %q", tlsCert.Name, gcpCertName)
				visitedCertMap[gcpCertName] = fmt.Sprintf("certificate:%q", gcpCertName)
				result = append(result, cert)
				continue
			}
		}
		// Controller needs to create the certificate, no need to check if it exists and delete. If it did exist, it
		// would have been listed in the populateSSLCert function and matched in the check above.
		glog.V(2).Infof("Creating new sslCertificate %q for LB %q", gcpCertName, l.Name)
		cert, err := l.cloud.CreateSslCertificate(&compute.SslCertificate{
			Name:        gcpCertName,
			Certificate: ingCert,
			PrivateKey:  ingKey,
		})
		if err != nil {
			glog.Errorf("Failed to create new sslCertificate %q for %q - %v", gcpCertName, l.Name, err)
			failedCerts = append(failedCerts, gcpCertName+" Error:"+err.Error())
			continue
		}
		visitedCertMap[gcpCertName] = fmt.Sprintf("secret:%q", tlsCert.Name)
		result = append(result, cert)
	}

	// Save the old certs for cleanup after we update the target proxy.
	if len(failedCerts) > 0 {
		return result, fmt.Errorf("Cert creation failures - %s", strings.Join(failedCerts, ","))
	}
	return result, nil
}

// getSslCertificates fetches GCE SslCertificate resources by names.
func (l *L7) getSslCertificates(names []string) ([]*compute.SslCertificate, error) {
	var result []*compute.SslCertificate
	var failedCerts []string
	for _, name := range names {
		// Ask GCE for the cert, checking for problems and existence.
		cert, err := l.cloud.GetSslCertificate(name)
		if err != nil {
			failedCerts = append(failedCerts, name+": "+err.Error())
			l.recorder.Eventf(l.runtimeInfo.Ingress, corev1.EventTypeNormal, SslCertificateMissing, err.Error())
			continue
		}
		if cert == nil {
			failedCerts = append(failedCerts, name+": unable to find existing SslCertificate")
			continue
		}

		glog.V(2).Infof("Using existing SslCertificate %v for %v", name, l.Name)
		result = append(result, cert)
	}
	if len(failedCerts) != 0 {
		return result, fmt.Errorf("Errors - %s", strings.Join(failedCerts, ","))
	}

	return result, nil
}

// getManagedCertificates fetches SslCertificates specified via managed-certificates annotation.
func (l *L7) getManagedCertificates() ([]*compute.SslCertificate, bool, error) {
	if l.runtimeInfo.ManagedCertificates == "" {
		return nil, false, nil
	}

	mcrtsNames := utils.SplitAnnotation(l.runtimeInfo.ManagedCertificates)
	req, err := labels.NewRequirement("metadata.name", selection.In, mcrtsNames)
	if err != nil {
		return nil, true, err
	}

	sel := labels.NewSelector()
	sel.Add(*req)
	mcrts, err := l.mcrt.ManagedCertificates(l.runtimeInfo.Ingress.Namespace).List(sel)
	if err != nil {
		return nil, true, err
	}

	var names []string
	for _, mcrt := range mcrts {
		if mcrt.Status.CertificateName != "" {
			names = append(names, mcrt.Status.CertificateName)
		}
	}

	sslCerts, err := l.getSslCertificates(names)
	if err != nil {
		return sslCerts, true, fmt.Errorf("managed-certificates errors: %s", err.Error())
	}

	return sslCerts, true, nil
}

// getPreSharedCertificates fetches SslCertificates specified via pre-shared-cert annotation.
func (l *L7) getPreSharedCertificates() (bool, []*compute.SslCertificate, error) {
	if l.runtimeInfo.TLSName == "" {
		return false, nil, nil
	}
	sslCerts, err := l.getSslCertificates(utils.SplitAnnotation(l.runtimeInfo.TLSName))

	if err != nil {
		return true, sslCerts, fmt.Errorf("pre-shared-cert errors: %s", err.Error())
	}

	return true, sslCerts, nil
}

func getMapfromCertList(certs []*compute.SslCertificate) map[string]*compute.SslCertificate {
	if len(certs) == 0 {
		return nil
	}
	certMap := make(map[string]*compute.SslCertificate)
	for _, cert := range certs {
		certMap[cert.Name] = cert
	}
	return certMap
}

// getIngressManagedSslCerts fetches SslCertificate resources created and managed by this load balancer
// instance. These SslCertificate resources were created based on kubernetes secrets in Ingress
// configuration.
func (l *L7) getIngressManagedSslCerts() ([]*compute.SslCertificate, error) {
	var result []*compute.SslCertificate

	// Currently we list all certs available in gcloud and filter the ones managed by this loadbalancer instance. This is
	// to make sure we garbage collect any old certs that this instance might have lost track of due to crashes.
	// Can be a performance issue if there are too many global certs, default quota is only 10.
	certs, err := l.cloud.ListSslCertificates()
	if err != nil {
		return nil, err
	}
	for _, c := range certs {
		if l.namer.IsCertUsedForLB(l.Name, c.Name) {
			glog.V(4).Infof("Populating ssl cert %s for l7 %s", c.Name, l.Name)
			result = append(result, c)
		}
	}
	if len(result) == 0 {
		// Check for legacy cert since that follows a different naming convention
		glog.V(4).Infof("Looking for legacy ssl certs")
		expectedCertLinks, err := l.getSslCertLinkInUse()
		if err != nil {
			// Return nil if target proxy doesn't exist.
			return nil, utils.IgnoreHTTPNotFound(err)
		}
		for _, link := range expectedCertLinks {
			// Retrieve the certificate and ignore error if certificate wasn't found
			name, err := utils.KeyName(link)
			if err != nil {
				glog.Warningf("error parsing cert name: %v", err)
				continue
			}

			if !l.namer.IsLegacySSLCert(l.Name, name) {
				continue
			}
			cert, _ := l.cloud.GetSslCertificate(name)
			if cert != nil {
				glog.V(4).Infof("Populating legacy ssl cert %s for l7 %s", cert.Name, l.Name)
				result = append(result, cert)
			}
		}
	}
	return result, nil
}

func (l *L7) deleteOldSSLCerts() {
	if len(l.oldSSLCerts) == 0 {
		return
	}
	certsMap := getMapfromCertList(l.sslCerts)
	for _, cert := range l.oldSSLCerts {
		if !l.namer.IsCertUsedForLB(l.Name, cert.Name) && !l.namer.IsLegacySSLCert(l.Name, cert.Name) {
			// retain cert if it is managed by GCE(non-ingress)
			continue
		}
		if _, ok := certsMap[cert.Name]; ok {
			// cert found in current map
			continue
		}
		glog.V(3).Infof("Cleaning up old SSL Certificate %s", cert.Name)
		if certErr := utils.IgnoreHTTPNotFound(l.cloud.DeleteSslCertificate(cert.Name)); certErr != nil {
			glog.Errorf("Old cert delete failed - %v", certErr)
		}
	}
}

// Returns true if the input array of certs is identical to the certs in the L7 config.
// Returns false if there is any mismatch
func (l *L7) compareCerts(certLinks []string) bool {
	certsMap := getMapfromCertList(l.sslCerts)
	if len(certLinks) != len(certsMap) {
		glog.V(4).Infof("Loadbalancer has %d certs, target proxy has %d certs", len(certsMap), len(certLinks))
		return false
	}

	for _, link := range certLinks {
		certName, err := utils.KeyName(link)
		if err != nil {
			glog.Warningf("Cannot get cert name: %v", err)
			return false
		}

		if cert, ok := certsMap[certName]; !ok {
			glog.V(4).Infof("Cannot find cert with name %s in certsMap %+v", certName, certsMap)
			return false
		} else if ok && !utils.EqualResourceIDs(link, cert.SelfLink) {
			glog.V(4).Infof("Selflink compare failed for certs - %s in loadbalancer, %s in targetproxy", cert.SelfLink, link)
			return false
		}
	}
	return true
}

func GetCertHash(contents string) string {
	return fmt.Sprintf("%x", sha256.Sum256([]byte(contents)))[:16]
}

func toCertNames(certs []*compute.SslCertificate) (names []string) {
	for _, v := range certs {
		names = append(names, v.Name)
	}
	return names
}
<commit_msg>Implement support for ManagedCertificate CRD: rename method creating SslCertificate resources<commit_after>/*
Copyright 2018 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 loadbalancers

import (
	"crypto/sha256"
	"fmt"
	"strings"

	"github.com/golang/glog"
	compute "google.golang.org/api/compute/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/selection"
	"k8s.io/ingress-gce/pkg/flags"
	"k8s.io/ingress-gce/pkg/utils"
)

const SslCertificateMissing = "SslCertificateMissing"

func (l *L7) checkSSLCert() error {
	if flags.F.Features.ManagedCertificates {
		// Handle annotation managed-certificates
		managedSslCerts, used, err := l.getManagedCertificates()
		if used {
			l.sslCerts = managedSslCerts
			return err
		}
	}

	// Handle annotation pre-shared-cert
	used, preSharedSslCerts, err := l.getPreSharedCertificates()
	if used {
		l.sslCerts = preSharedSslCerts
		return err
	}

	// Get updated value of certificate for comparison
	existingSecretsSslCerts, err := l.getIngressManagedSslCerts()
	if err != nil {
		return err
	}

	l.oldSSLCerts = existingSecretsSslCerts
	secretsSslCerts, err := l.createSslCertificates(existingSecretsSslCerts)
	l.sslCerts = secretsSslCerts
	return err
}

// createSslCertificates creates SslCertificates based on kubernetes secrets in Ingress configuration.
func (l *L7) createSslCertificates(existingCerts []*compute.SslCertificate) ([]*compute.SslCertificate, error) {
	var result []*compute.SslCertificate

	existingCertsMap := getMapfromCertList(existingCerts)

	// mapping of currently configured certs
	visitedCertMap := make(map[string]string)
	var failedCerts []string

	for _, tlsCert := range l.runtimeInfo.TLS {
		ingCert := tlsCert.Cert
		ingKey := tlsCert.Key
		gcpCertName := l.namer.SSLCertName(l.Name, tlsCert.CertHash)

		if addedBy, exists := visitedCertMap[gcpCertName]; exists {
			glog.V(3).Infof("Secret %q has a certificate already used by %v", tlsCert.Name, addedBy)
			continue
		}

		// PrivateKey is write only, so compare certs alone. We're assuming that
		// no one will change just the key. We can remember the key and compare,
		// but a bug could end up leaking it, which feels worse.
		// If the cert contents have changed, its hash would be different, so would be the cert name. So it is enough
		// to check if this cert name exists in the map.
		if existingCertsMap != nil {
			if cert, ok := existingCertsMap[gcpCertName]; ok {
				glog.V(3).Infof("Secret %q already exists as certificate %q", tlsCert.Name, gcpCertName)
				visitedCertMap[gcpCertName] = fmt.Sprintf("certificate:%q", gcpCertName)
				result = append(result, cert)
				continue
			}
		}
		// Controller needs to create the certificate, no need to check if it exists and delete. If it did exist, it
		// would have been listed in the populateSSLCert function and matched in the check above.
		glog.V(2).Infof("Creating new sslCertificate %q for LB %q", gcpCertName, l.Name)
		cert, err := l.cloud.CreateSslCertificate(&compute.SslCertificate{
			Name:        gcpCertName,
			Certificate: ingCert,
			PrivateKey:  ingKey,
		})
		if err != nil {
			glog.Errorf("Failed to create new sslCertificate %q for %q - %v", gcpCertName, l.Name, err)
			failedCerts = append(failedCerts, gcpCertName+" Error:"+err.Error())
			continue
		}
		visitedCertMap[gcpCertName] = fmt.Sprintf("secret:%q", tlsCert.Name)
		result = append(result, cert)
	}

	// Save the old certs for cleanup after we update the target proxy.
	if len(failedCerts) > 0 {
		return result, fmt.Errorf("Cert creation failures - %s", strings.Join(failedCerts, ","))
	}
	return result, nil
}

// getSslCertificates fetches GCE SslCertificate resources by names.
func (l *L7) getSslCertificates(names []string) ([]*compute.SslCertificate, error) {
	var result []*compute.SslCertificate
	var failedCerts []string
	for _, name := range names {
		// Ask GCE for the cert, checking for problems and existence.
		cert, err := l.cloud.GetSslCertificate(name)
		if err != nil {
			failedCerts = append(failedCerts, name+": "+err.Error())
			l.recorder.Eventf(l.runtimeInfo.Ingress, corev1.EventTypeNormal, SslCertificateMissing, err.Error())
			continue
		}
		if cert == nil {
			failedCerts = append(failedCerts, name+": unable to find existing SslCertificate")
			continue
		}

		glog.V(2).Infof("Using existing SslCertificate %v for %v", name, l.Name)
		result = append(result, cert)
	}
	if len(failedCerts) != 0 {
		return result, fmt.Errorf("Errors - %s", strings.Join(failedCerts, ","))
	}

	return result, nil
}

// getManagedCertificates fetches SslCertificates specified via managed-certificates annotation.
func (l *L7) getManagedCertificates() ([]*compute.SslCertificate, bool, error) {
	if l.runtimeInfo.ManagedCertificates == "" {
		return nil, false, nil
	}

	mcrtsNames := utils.SplitAnnotation(l.runtimeInfo.ManagedCertificates)
	req, err := labels.NewRequirement("metadata.name", selection.In, mcrtsNames)
	if err != nil {
		return nil, true, err
	}

	sel := labels.NewSelector()
	sel.Add(*req)
	mcrts, err := l.mcrt.ManagedCertificates(l.runtimeInfo.Ingress.Namespace).List(sel)
	if err != nil {
		return nil, true, err
	}

	var names []string
	for _, mcrt := range mcrts {
		if mcrt.Status.CertificateName != "" {
			names = append(names, mcrt.Status.CertificateName)
		}
	}

	sslCerts, err := l.getSslCertificates(names)
	if err != nil {
		return sslCerts, true, fmt.Errorf("managed-certificates errors: %s", err.Error())
	}

	return sslCerts, true, nil
}

// getPreSharedCertificates fetches SslCertificates specified via pre-shared-cert annotation.
func (l *L7) getPreSharedCertificates() (bool, []*compute.SslCertificate, error) {
	if l.runtimeInfo.TLSName == "" {
		return false, nil, nil
	}
	sslCerts, err := l.getSslCertificates(utils.SplitAnnotation(l.runtimeInfo.TLSName))

	if err != nil {
		return true, sslCerts, fmt.Errorf("pre-shared-cert errors: %s", err.Error())
	}

	return true, sslCerts, nil
}

func getMapfromCertList(certs []*compute.SslCertificate) map[string]*compute.SslCertificate {
	if len(certs) == 0 {
		return nil
	}
	certMap := make(map[string]*compute.SslCertificate)
	for _, cert := range certs {
		certMap[cert.Name] = cert
	}
	return certMap
}

// getIngressManagedSslCerts fetches SslCertificate resources created and managed by this load balancer
// instance. These SslCertificate resources were created based on kubernetes secrets in Ingress
// configuration.
func (l *L7) getIngressManagedSslCerts() ([]*compute.SslCertificate, error) {
	var result []*compute.SslCertificate

	// Currently we list all certs available in gcloud and filter the ones managed by this loadbalancer instance. This is
	// to make sure we garbage collect any old certs that this instance might have lost track of due to crashes.
	// Can be a performance issue if there are too many global certs, default quota is only 10.
	certs, err := l.cloud.ListSslCertificates()
	if err != nil {
		return nil, err
	}
	for _, c := range certs {
		if l.namer.IsCertUsedForLB(l.Name, c.Name) {
			glog.V(4).Infof("Populating ssl cert %s for l7 %s", c.Name, l.Name)
			result = append(result, c)
		}
	}
	if len(result) == 0 {
		// Check for legacy cert since that follows a different naming convention
		glog.V(4).Infof("Looking for legacy ssl certs")
		expectedCertLinks, err := l.getSslCertLinkInUse()
		if err != nil {
			// Return nil if target proxy doesn't exist.
			return nil, utils.IgnoreHTTPNotFound(err)
		}
		for _, link := range expectedCertLinks {
			// Retrieve the certificate and ignore error if certificate wasn't found
			name, err := utils.KeyName(link)
			if err != nil {
				glog.Warningf("error parsing cert name: %v", err)
				continue
			}

			if !l.namer.IsLegacySSLCert(l.Name, name) {
				continue
			}
			cert, _ := l.cloud.GetSslCertificate(name)
			if cert != nil {
				glog.V(4).Infof("Populating legacy ssl cert %s for l7 %s", cert.Name, l.Name)
				result = append(result, cert)
			}
		}
	}
	return result, nil
}

func (l *L7) deleteOldSSLCerts() {
	if len(l.oldSSLCerts) == 0 {
		return
	}
	certsMap := getMapfromCertList(l.sslCerts)
	for _, cert := range l.oldSSLCerts {
		if !l.namer.IsCertUsedForLB(l.Name, cert.Name) && !l.namer.IsLegacySSLCert(l.Name, cert.Name) {
			// retain cert if it is managed by GCE(non-ingress)
			continue
		}
		if _, ok := certsMap[cert.Name]; ok {
			// cert found in current map
			continue
		}
		glog.V(3).Infof("Cleaning up old SSL Certificate %s", cert.Name)
		if certErr := utils.IgnoreHTTPNotFound(l.cloud.DeleteSslCertificate(cert.Name)); certErr != nil {
			glog.Errorf("Old cert delete failed - %v", certErr)
		}
	}
}

// Returns true if the input array of certs is identical to the certs in the L7 config.
// Returns false if there is any mismatch
func (l *L7) compareCerts(certLinks []string) bool {
	certsMap := getMapfromCertList(l.sslCerts)
	if len(certLinks) != len(certsMap) {
		glog.V(4).Infof("Loadbalancer has %d certs, target proxy has %d certs", len(certsMap), len(certLinks))
		return false
	}

	for _, link := range certLinks {
		certName, err := utils.KeyName(link)
		if err != nil {
			glog.Warningf("Cannot get cert name: %v", err)
			return false
		}

		if cert, ok := certsMap[certName]; !ok {
			glog.V(4).Infof("Cannot find cert with name %s in certsMap %+v", certName, certsMap)
			return false
		} else if ok && !utils.EqualResourceIDs(link, cert.SelfLink) {
			glog.V(4).Infof("Selflink compare failed for certs - %s in loadbalancer, %s in targetproxy", cert.SelfLink, link)
			return false
		}
	}
	return true
}

func GetCertHash(contents string) string {
	return fmt.Sprintf("%x", sha256.Sum256([]byte(contents)))[:16]
}

func toCertNames(certs []*compute.SslCertificate) (names []string) {
	for _, v := range certs {
		names = append(names, v.Name)
	}
	return names
}
<|endoftext|>


<commit_before>/*
Copyright 2016 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 etcd3

import (
	"context"
	"fmt"
	"sync"
	"testing"
	"time"

	"go.etcd.io/etcd/api/v3/mvccpb"
	clientv3 "go.etcd.io/etcd/client/v3"
	storagetesting "k8s.io/apiserver/pkg/storage/testing"

	"k8s.io/apimachinery/pkg/api/apitesting"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/apimachinery/pkg/watch"
	"k8s.io/apiserver/pkg/apis/example"
	examplev1 "k8s.io/apiserver/pkg/apis/example/v1"
	"k8s.io/apiserver/pkg/storage"
	"k8s.io/apiserver/pkg/storage/etcd3/testserver"
)

func TestWatch(t *testing.T) {
	ctx, store, _ := testSetup(t)
	storagetesting.RunTestWatch(ctx, t, store)
}

func TestDeleteTriggerWatch(t *testing.T) {
	ctx, store, _ := testSetup(t)
	storagetesting.RunTestDeleteTriggerWatch(ctx, t, store)
}

// TestWatchFromZero tests that
// - watch from 0 should sync up and grab the object added before
// - watch from 0 is able to return events for objects whose previous version has been compacted
func TestWatchFromZero(t *testing.T) {
	ctx, store, client := testSetup(t)
	key, storedObj := storagetesting.TestPropogateStore(ctx, t, store, &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "ns"}})

	w, err := store.Watch(ctx, key, storage.ListOptions{ResourceVersion: "0", Predicate: storage.Everything})
	if err != nil {
		t.Fatalf("Watch failed: %v", err)
	}
	storagetesting.TestCheckResult(t, watch.Added, w, storedObj)
	w.Stop()

	// Update
	out := &example.Pod{}
	err = store.GuaranteedUpdate(ctx, key, out, true, nil, storage.SimpleUpdate(
		func(runtime.Object) (runtime.Object, error) {
			return &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "ns", Annotations: map[string]string{"a": "1"}}}, nil
		}), nil)
	if err != nil {
		t.Fatalf("GuaranteedUpdate failed: %v", err)
	}

	// Make sure when we watch from 0 we receive an ADDED event
	w, err = store.Watch(ctx, key, storage.ListOptions{ResourceVersion: "0", Predicate: storage.Everything})
	if err != nil {
		t.Fatalf("Watch failed: %v", err)
	}
	storagetesting.TestCheckResult(t, watch.Added, w, out)
	w.Stop()

	// Update again
	out = &example.Pod{}
	err = store.GuaranteedUpdate(ctx, key, out, true, nil, storage.SimpleUpdate(
		func(runtime.Object) (runtime.Object, error) {
			return &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "ns"}}, nil
		}), nil)
	if err != nil {
		t.Fatalf("GuaranteedUpdate failed: %v", err)
	}

	// Compact previous versions
	revToCompact, err := store.versioner.ParseResourceVersion(out.ResourceVersion)
	if err != nil {
		t.Fatalf("Error converting %q to an int: %v", storedObj.ResourceVersion, err)
	}
	_, err = client.Compact(ctx, int64(revToCompact), clientv3.WithCompactPhysical())
	if err != nil {
		t.Fatalf("Error compacting: %v", err)
	}

	// Make sure we can still watch from 0 and receive an ADDED event
	w, err = store.Watch(ctx, key, storage.ListOptions{ResourceVersion: "0", Predicate: storage.Everything})
	if err != nil {
		t.Fatalf("Watch failed: %v", err)
	}
	storagetesting.TestCheckResult(t, watch.Added, w, out)
}

// TestWatchFromNoneZero tests that
// - watch from non-0 should just watch changes after given version
func TestWatchFromNoneZero(t *testing.T) {
	ctx, store, _ := testSetup(t)
	storagetesting.RunTestWatchFromNoneZero(ctx, t, store)
}

func TestWatchError(t *testing.T) {
	// this codec fails on decodes, which will bubble up so we can verify the behavior
	invalidCodec := &testCodec{apitesting.TestCodec(codecs, examplev1.SchemeGroupVersion)}
	ctx, invalidStore, client := testSetup(t, withCodec(invalidCodec))
	w, err := invalidStore.Watch(ctx, "/abc", storage.ListOptions{ResourceVersion: "0", Predicate: storage.Everything})
	if err != nil {
		t.Fatalf("Watch failed: %v", err)
	}
	codec := apitesting.TestCodec(codecs, examplev1.SchemeGroupVersion)
	_, validStore, _ := testSetup(t, withCodec(codec), withClient(client))
	if err := validStore.GuaranteedUpdate(ctx, "/abc", &example.Pod{}, true, nil, storage.SimpleUpdate(
		func(runtime.Object) (runtime.Object, error) {
			return &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, nil
		}), nil); err != nil {
		t.Fatalf("GuaranteedUpdate failed: %v", err)
	}
	storagetesting.TestCheckEventType(t, watch.Error, w)
}

func TestWatchContextCancel(t *testing.T) {
	ctx, store, _ := testSetup(t)
	canceledCtx, cancel := context.WithCancel(ctx)
	cancel()
	// When we watch with a canceled context, we should detect that it's context canceled.
	// We won't take it as error and also close the watcher.
	w, err := store.Watch(canceledCtx, "/abc", storage.ListOptions{
		ResourceVersion:      "0",
		Predicate:            storage.Everything,
	})
	if err != nil {
		t.Fatal(err)
	}

	select {
	case _, ok := <-w.ResultChan():
		if ok {
			t.Error("ResultChan() should be closed")
		}
	case <-time.After(wait.ForeverTestTimeout):
		t.Errorf("timeout after %v", wait.ForeverTestTimeout)
	}
}

func TestWatchErrResultNotBlockAfterCancel(t *testing.T) {
	origCtx, store, _ := testSetup(t)
	ctx, cancel := context.WithCancel(origCtx)
	w := store.watcher.createWatchChan(ctx, "/abc", 0, false, false, storage.Everything)
	// make resutlChan and errChan blocking to ensure ordering.
	w.resultChan = make(chan watch.Event)
	w.errChan = make(chan error)
	// The event flow goes like:
	// - first we send an error, it should block on resultChan.
	// - Then we cancel ctx. The blocking on resultChan should be freed up
	//   and run() goroutine should return.
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		w.run()
		wg.Done()
	}()
	w.errChan <- fmt.Errorf("some error")
	cancel()
	wg.Wait()
}

func TestWatchDeleteEventObjectHaveLatestRV(t *testing.T) {
	ctx, store, client := testSetup(t)
	key, storedObj := storagetesting.TestPropogateStore(ctx, t, store, &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}})

	w, err := store.Watch(ctx, key, storage.ListOptions{ResourceVersion: storedObj.ResourceVersion, Predicate: storage.Everything})
	if err != nil {
		t.Fatalf("Watch failed: %v", err)
	}
	rv, err := APIObjectVersioner{}.ObjectResourceVersion(storedObj)
	if err != nil {
		t.Fatalf("failed to parse resourceVersion on stored object: %v", err)
	}
	etcdW := client.Watch(ctx, key, clientv3.WithRev(int64(rv)))

	if err := store.Delete(ctx, key, &example.Pod{}, &storage.Preconditions{}, storage.ValidateAllObjectFunc, nil); err != nil {
		t.Fatalf("Delete failed: %v", err)
	}

	var e watch.Event
	watchCtx, _ := context.WithTimeout(ctx, wait.ForeverTestTimeout)
	select {
	case e = <-w.ResultChan():
	case <-watchCtx.Done():
		t.Fatalf("timed out waiting for watch event")
	}
	deletedRV, err := deletedRevision(watchCtx, etcdW)
	if err != nil {
		t.Fatalf("did not see delete event in raw watch: %v", err)
	}
	watchedDeleteObj := e.Object.(*example.Pod)

	watchedDeleteRev, err := store.versioner.ParseResourceVersion(watchedDeleteObj.ResourceVersion)
	if err != nil {
		t.Fatalf("ParseWatchResourceVersion failed: %v", err)
	}
	if int64(watchedDeleteRev) != deletedRV {
		t.Errorf("Object from delete event have version: %v, should be the same as etcd delete's mod rev: %d",
			watchedDeleteRev, deletedRV)
	}
}

func deletedRevision(ctx context.Context, watch <-chan clientv3.WatchResponse) (int64, error) {
	for {
		select {
		case <-ctx.Done():
			return 0, ctx.Err()
		case wres := <-watch:
			for _, evt := range wres.Events {
				if evt.Type == mvccpb.DELETE && evt.Kv != nil {
					return evt.Kv.ModRevision, nil
				}
			}
		}
	}
}

func TestWatchInitializationSignal(t *testing.T) {
	ctx, store, _ := testSetup(t)
	storagetesting.RunTestWatchInitializationSignal(ctx, t, store)
}

func TestProgressNotify(t *testing.T) {
	clusterConfig := testserver.NewTestConfig(t)
	clusterConfig.ExperimentalWatchProgressNotifyInterval = time.Second
	ctx, store, _ := testSetup(t, withClientConfig(clusterConfig))

	key := "/somekey"
	input := &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "name"}}
	out := &example.Pod{}
	if err := store.Create(ctx, key, input, out, 0); err != nil {
		t.Fatalf("Create failed: %v", err)
	}
	validateResourceVersion := resourceVersionNotOlderThan(out.ResourceVersion)

	opts := storage.ListOptions{
		ResourceVersion: out.ResourceVersion,
		Predicate:       storage.Everything,
		ProgressNotify:  true,
	}
	w, err := store.Watch(ctx, key, opts)
	if err != nil {
		t.Fatalf("Watch failed: %v", err)
	}

	// when we send a bookmark event, the client expects the event to contain an
	// object of the correct type, but with no fields set other than the resourceVersion
	storagetesting.TestCheckResultFunc(t, watch.Bookmark, w, func(object runtime.Object) error {
		// first, check that we have the correct resource version
		obj, ok := object.(metav1.Object)
		if !ok {
			return fmt.Errorf("got %T, not metav1.Object", object)
		}
		if err := validateResourceVersion(obj.GetResourceVersion()); err != nil {
			return err
		}

		// then, check that we have the right type and content
		pod, ok := object.(*example.Pod)
		if !ok {
			return fmt.Errorf("got %T, not *example.Pod", object)
		}
		pod.ResourceVersion = ""
		storagetesting.ExpectNoDiff(t, "bookmark event should contain an object with no fields set other than resourceVersion", newPod(), pod)
		return nil
	})
}

type testCodec struct {
	runtime.Codec
}

func (c *testCodec) Decode(data []byte, defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
	return nil, nil, errTestingDecode
}

// resourceVersionNotOlderThan returns a function to validate resource versions. Resource versions
// referring to points in logical time before the sentinel generate an error. All logical times as
// new as the sentinel or newer generate no error.
func resourceVersionNotOlderThan(sentinel string) func(string) error {
	return func(resourceVersion string) error {
		objectVersioner := APIObjectVersioner{}
		actualRV, err := objectVersioner.ParseResourceVersion(resourceVersion)
		if err != nil {
			return err
		}
		expectedRV, err := objectVersioner.ParseResourceVersion(sentinel)
		if err != nil {
			return err
		}
		if actualRV < expectedRV {
			return fmt.Errorf("expected a resourceVersion no smaller than than %d, but got %d", expectedRV, actualRV)
		}
		return nil
	}
}
<commit_msg>etcd3/store: call a generic cancelled watch test<commit_after>/*
Copyright 2016 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 etcd3

import (
	"context"
	"fmt"
	"sync"
	"testing"
	"time"

	"go.etcd.io/etcd/api/v3/mvccpb"
	clientv3 "go.etcd.io/etcd/client/v3"
	storagetesting "k8s.io/apiserver/pkg/storage/testing"

	"k8s.io/apimachinery/pkg/api/apitesting"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/apimachinery/pkg/watch"
	"k8s.io/apiserver/pkg/apis/example"
	examplev1 "k8s.io/apiserver/pkg/apis/example/v1"
	"k8s.io/apiserver/pkg/storage"
	"k8s.io/apiserver/pkg/storage/etcd3/testserver"
)

func TestWatch(t *testing.T) {
	ctx, store, _ := testSetup(t)
	storagetesting.RunTestWatch(ctx, t, store)
}

func TestDeleteTriggerWatch(t *testing.T) {
	ctx, store, _ := testSetup(t)
	storagetesting.RunTestDeleteTriggerWatch(ctx, t, store)
}

// TestWatchFromZero tests that
// - watch from 0 should sync up and grab the object added before
// - watch from 0 is able to return events for objects whose previous version has been compacted
func TestWatchFromZero(t *testing.T) {
	ctx, store, client := testSetup(t)
	key, storedObj := storagetesting.TestPropogateStore(ctx, t, store, &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "ns"}})

	w, err := store.Watch(ctx, key, storage.ListOptions{ResourceVersion: "0", Predicate: storage.Everything})
	if err != nil {
		t.Fatalf("Watch failed: %v", err)
	}
	storagetesting.TestCheckResult(t, watch.Added, w, storedObj)
	w.Stop()

	// Update
	out := &example.Pod{}
	err = store.GuaranteedUpdate(ctx, key, out, true, nil, storage.SimpleUpdate(
		func(runtime.Object) (runtime.Object, error) {
			return &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "ns", Annotations: map[string]string{"a": "1"}}}, nil
		}), nil)
	if err != nil {
		t.Fatalf("GuaranteedUpdate failed: %v", err)
	}

	// Make sure when we watch from 0 we receive an ADDED event
	w, err = store.Watch(ctx, key, storage.ListOptions{ResourceVersion: "0", Predicate: storage.Everything})
	if err != nil {
		t.Fatalf("Watch failed: %v", err)
	}
	storagetesting.TestCheckResult(t, watch.Added, w, out)
	w.Stop()

	// Update again
	out = &example.Pod{}
	err = store.GuaranteedUpdate(ctx, key, out, true, nil, storage.SimpleUpdate(
		func(runtime.Object) (runtime.Object, error) {
			return &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "ns"}}, nil
		}), nil)
	if err != nil {
		t.Fatalf("GuaranteedUpdate failed: %v", err)
	}

	// Compact previous versions
	revToCompact, err := store.versioner.ParseResourceVersion(out.ResourceVersion)
	if err != nil {
		t.Fatalf("Error converting %q to an int: %v", storedObj.ResourceVersion, err)
	}
	_, err = client.Compact(ctx, int64(revToCompact), clientv3.WithCompactPhysical())
	if err != nil {
		t.Fatalf("Error compacting: %v", err)
	}

	// Make sure we can still watch from 0 and receive an ADDED event
	w, err = store.Watch(ctx, key, storage.ListOptions{ResourceVersion: "0", Predicate: storage.Everything})
	if err != nil {
		t.Fatalf("Watch failed: %v", err)
	}
	storagetesting.TestCheckResult(t, watch.Added, w, out)
}

// TestWatchFromNoneZero tests that
// - watch from non-0 should just watch changes after given version
func TestWatchFromNoneZero(t *testing.T) {
	ctx, store, _ := testSetup(t)
	storagetesting.RunTestWatchFromNoneZero(ctx, t, store)
}

func TestWatchError(t *testing.T) {
	// this codec fails on decodes, which will bubble up so we can verify the behavior
	invalidCodec := &testCodec{apitesting.TestCodec(codecs, examplev1.SchemeGroupVersion)}
	ctx, invalidStore, client := testSetup(t, withCodec(invalidCodec))
	w, err := invalidStore.Watch(ctx, "/abc", storage.ListOptions{ResourceVersion: "0", Predicate: storage.Everything})
	if err != nil {
		t.Fatalf("Watch failed: %v", err)
	}
	codec := apitesting.TestCodec(codecs, examplev1.SchemeGroupVersion)
	_, validStore, _ := testSetup(t, withCodec(codec), withClient(client))
	if err := validStore.GuaranteedUpdate(ctx, "/abc", &example.Pod{}, true, nil, storage.SimpleUpdate(
		func(runtime.Object) (runtime.Object, error) {
			return &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}}, nil
		}), nil); err != nil {
		t.Fatalf("GuaranteedUpdate failed: %v", err)
	}
	storagetesting.TestCheckEventType(t, watch.Error, w)
}

func TestWatchContextCancel(t *testing.T) {
	ctx, store, _ := testSetup(t)
	RunTestWatchContextCancel(ctx, t, store)
}

func RunTestWatchContextCancel(ctx context.Context, t *testing.T, store storage.Interface) {
	canceledCtx, cancel := context.WithCancel(ctx)
	cancel()
	// When we watch with a canceled context, we should detect that it's context canceled.
	// We won't take it as error and also close the watcher.
	w, err := store.Watch(canceledCtx, "/abc", storage.ListOptions{
		ResourceVersion:      "0",
		Predicate:            storage.Everything,
	})
	if err != nil {
		t.Fatal(err)
	}

	select {
	case _, ok := <-w.ResultChan():
		if ok {
			t.Error("ResultChan() should be closed")
		}
	case <-time.After(wait.ForeverTestTimeout):
		t.Errorf("timeout after %v", wait.ForeverTestTimeout)
	}
}

func TestWatchErrResultNotBlockAfterCancel(t *testing.T) {
	origCtx, store, _ := testSetup(t)
	ctx, cancel := context.WithCancel(origCtx)
	w := store.watcher.createWatchChan(ctx, "/abc", 0, false, false, storage.Everything)
	// make resutlChan and errChan blocking to ensure ordering.
	w.resultChan = make(chan watch.Event)
	w.errChan = make(chan error)
	// The event flow goes like:
	// - first we send an error, it should block on resultChan.
	// - Then we cancel ctx. The blocking on resultChan should be freed up
	//   and run() goroutine should return.
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		w.run()
		wg.Done()
	}()
	w.errChan <- fmt.Errorf("some error")
	cancel()
	wg.Wait()
}

func TestWatchDeleteEventObjectHaveLatestRV(t *testing.T) {
	ctx, store, client := testSetup(t)
	key, storedObj := storagetesting.TestPropogateStore(ctx, t, store, &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "foo"}})

	w, err := store.Watch(ctx, key, storage.ListOptions{ResourceVersion: storedObj.ResourceVersion, Predicate: storage.Everything})
	if err != nil {
		t.Fatalf("Watch failed: %v", err)
	}
	rv, err := APIObjectVersioner{}.ObjectResourceVersion(storedObj)
	if err != nil {
		t.Fatalf("failed to parse resourceVersion on stored object: %v", err)
	}
	etcdW := client.Watch(ctx, key, clientv3.WithRev(int64(rv)))

	if err := store.Delete(ctx, key, &example.Pod{}, &storage.Preconditions{}, storage.ValidateAllObjectFunc, nil); err != nil {
		t.Fatalf("Delete failed: %v", err)
	}

	var e watch.Event
	watchCtx, _ := context.WithTimeout(ctx, wait.ForeverTestTimeout)
	select {
	case e = <-w.ResultChan():
	case <-watchCtx.Done():
		t.Fatalf("timed out waiting for watch event")
	}
	deletedRV, err := deletedRevision(watchCtx, etcdW)
	if err != nil {
		t.Fatalf("did not see delete event in raw watch: %v", err)
	}
	watchedDeleteObj := e.Object.(*example.Pod)

	watchedDeleteRev, err := store.versioner.ParseResourceVersion(watchedDeleteObj.ResourceVersion)
	if err != nil {
		t.Fatalf("ParseWatchResourceVersion failed: %v", err)
	}
	if int64(watchedDeleteRev) != deletedRV {
		t.Errorf("Object from delete event have version: %v, should be the same as etcd delete's mod rev: %d",
			watchedDeleteRev, deletedRV)
	}
}

func deletedRevision(ctx context.Context, watch <-chan clientv3.WatchResponse) (int64, error) {
	for {
		select {
		case <-ctx.Done():
			return 0, ctx.Err()
		case wres := <-watch:
			for _, evt := range wres.Events {
				if evt.Type == mvccpb.DELETE && evt.Kv != nil {
					return evt.Kv.ModRevision, nil
				}
			}
		}
	}
}

func TestWatchInitializationSignal(t *testing.T) {
	ctx, store, _ := testSetup(t)
	storagetesting.RunTestWatchInitializationSignal(ctx, t, store)
}

func TestProgressNotify(t *testing.T) {
	clusterConfig := testserver.NewTestConfig(t)
	clusterConfig.ExperimentalWatchProgressNotifyInterval = time.Second
	ctx, store, _ := testSetup(t, withClientConfig(clusterConfig))

	key := "/somekey"
	input := &example.Pod{ObjectMeta: metav1.ObjectMeta{Name: "name"}}
	out := &example.Pod{}
	if err := store.Create(ctx, key, input, out, 0); err != nil {
		t.Fatalf("Create failed: %v", err)
	}
	validateResourceVersion := resourceVersionNotOlderThan(out.ResourceVersion)

	opts := storage.ListOptions{
		ResourceVersion: out.ResourceVersion,
		Predicate:       storage.Everything,
		ProgressNotify:  true,
	}
	w, err := store.Watch(ctx, key, opts)
	if err != nil {
		t.Fatalf("Watch failed: %v", err)
	}

	// when we send a bookmark event, the client expects the event to contain an
	// object of the correct type, but with no fields set other than the resourceVersion
	storagetesting.TestCheckResultFunc(t, watch.Bookmark, w, func(object runtime.Object) error {
		// first, check that we have the correct resource version
		obj, ok := object.(metav1.Object)
		if !ok {
			return fmt.Errorf("got %T, not metav1.Object", object)
		}
		if err := validateResourceVersion(obj.GetResourceVersion()); err != nil {
			return err
		}

		// then, check that we have the right type and content
		pod, ok := object.(*example.Pod)
		if !ok {
			return fmt.Errorf("got %T, not *example.Pod", object)
		}
		pod.ResourceVersion = ""
		storagetesting.ExpectNoDiff(t, "bookmark event should contain an object with no fields set other than resourceVersion", newPod(), pod)
		return nil
	})
}

type testCodec struct {
	runtime.Codec
}

func (c *testCodec) Decode(data []byte, defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
	return nil, nil, errTestingDecode
}

// resourceVersionNotOlderThan returns a function to validate resource versions. Resource versions
// referring to points in logical time before the sentinel generate an error. All logical times as
// new as the sentinel or newer generate no error.
func resourceVersionNotOlderThan(sentinel string) func(string) error {
	return func(resourceVersion string) error {
		objectVersioner := APIObjectVersioner{}
		actualRV, err := objectVersioner.ParseResourceVersion(resourceVersion)
		if err != nil {
			return err
		}
		expectedRV, err := objectVersioner.ParseResourceVersion(sentinel)
		if err != nil {
			return err
		}
		if actualRV < expectedRV {
			return fmt.Errorf("expected a resourceVersion no smaller than than %d, but got %d", expectedRV, actualRV)
		}
		return nil
	}
}
<|endoftext|>


<commit_before>package forwardsec

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/asn1"
	"github.com/gokyle/dhkam"
	"github.com/kisom/gocrypto/chapter8/pks"
	"github.com/kisom/gocrypto/chapter9/authsym"
)

var PRNG = rand.Reader

const sharedKeyLen = 48

// IdentityKey represents a long-term identity key.
type IdentityKey struct {
	key *rsa.PrivateKey
}

// Public returns the public identity key.
func (id *IdentityKey) Public() []byte {
	cert, err := x509.MarshalPKIXPublicKey(&id.key.PublicKey)
	if err != nil {
		return nil
	}
	return cert
}

func NewIdentityKey() *IdentityKey {
	id := new(IdentityKey)

	var err error
	id.key, err = pks.GenerateKey()
	if err == nil {
		return id
	}
	return nil
}

func ImportPeerIdentity(in []byte) *rsa.PublicKey {
	if in == nil {
		return nil
	}
	cert, err := x509.ParsePKIXPublicKey(in)
	if err != nil {
		return nil
	}
	return cert.(*rsa.PublicKey)
}

// A SessionKey should be generated for each new session.
type SessionKey struct {
	key       *dhkam.PrivateKey
	signedKey []byte
	peer      *dhkam.PublicKey
}

type signedDHKey struct {
	Public    []byte
	Signature []byte
}

func (id *IdentityKey) NewSessionKey() *SessionKey {
	skey := new(SessionKey)

	var err error
	skey.key, err = dhkam.GenerateKey(PRNG)
	if err != nil {
		return nil
	}

	var sdhkey signedDHKey
	sdhkey.Public = skey.key.Export()
	sdhkey.Signature, err = pks.Sign(id.key, sdhkey.Public)
	if err != nil {
		return nil
	}

	skey.signedKey, err = asn1.Marshal(sdhkey)
	if err != nil {
		return nil
	}
	return skey
}

// Public should be used to export the signed public key to the client
func (skey *SessionKey) Public() []byte {
	return skey.signedKey
}

// PeerSessionKey reads the session key passed and checks the
// signature on it; if the signature is valid, it returns the peer's
// DH public key. On failure, it returns nil.
func (skey *SessionKey) PeerSessionKey(peer *rsa.PublicKey, session []byte) error {
	var signedKey signedDHKey
	_, err := asn1.Unmarshal(session, &signedKey)
	if err != nil {
		return err
	}

	if err = pks.Verify(peer, signedKey.Public, signedKey.Signature); err != nil {
		return err
	}

	pub, err := dhkam.ImportPublic(signedKey.Public)
	if err != nil {
		return err
	}
	skey.peer = pub
	return nil
}

func (skey *SessionKey) Decrypt(in []byte) ([]byte, error) {
	var ephem struct {
		Pub []byte
		CT  []byte
	}

	_, err := asn1.Unmarshal(in, &ephem)
	if err != nil {
		return nil, err
	}

	pub, err := dhkam.ImportPublic(ephem.Pub)
	if err != nil {
		return nil, err
	}

	shared, err := skey.key.SharedKey(PRNG, pub, sharedKeyLen)
	if err != nil {
		return nil, err
	}

	symkey := shared[:authsym.SymKeyLen]
	mackey := shared[authsym.SymKeyLen:]
	out, err := authsym.Decrypt(symkey, mackey, ephem.CT)
	if err != nil {
		return nil, err
	}
	return out, nil
}

func (skey *SessionKey) Encrypt(message []byte) ([]byte, error) {
	dhEphem, err := dhkam.GenerateKey(PRNG)
	if err != nil {
		return nil, err
	}

	shared, err := dhEphem.SharedKey(PRNG, skey.peer, sharedKeyLen)
	if err != nil {
		return nil, err
	}

	var ephem struct {
		Pub []byte
		CT  []byte
	}

	symkey := shared[:authsym.SymKeyLen]
	mackey := shared[authsym.SymKeyLen:]
	ephem.CT, err = authsym.Encrypt(symkey, mackey, message)
	if err != nil {
		return nil, err
	}

	ephem.Pub = dhEphem.Export()
	return asn1.Marshal(ephem)
}
<commit_msg>Clean up forwardsec example.<commit_after>// forwardsec is a demonstration of forward secrecy with RSA and
// DH. It uses RSA 3072-bit keys for identity, DH group 14 keys for
// encryption, and AES-128 in CTR mode with HMAC-SHA-256 message
// tags for ciphertexts.
//
// The basic flow is:
//   1. Generate a new identity key.
//
//   2. For each peer we are communicating with, generate a new
//      session key.
//
//   3. Send the peer the session key's Public value.
//
//   4. When the peer sends its Public session key value, call
//      the PeerSessionKey method on the session key.
//
//   5. Call the Encrypt and Decrypt methods as needed.
package forwardsec

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/asn1"
	"github.com/gokyle/dhkam"
	"github.com/kisom/gocrypto/chapter8/pks"
	"github.com/kisom/gocrypto/chapter9/authsym"
)

var PRNG = rand.Reader

const sharedKeyLen = 48

// IdentityKey represents a long-term identity key.
type IdentityKey struct {
	key *rsa.PrivateKey
}

// Public returns the public identity key.
func (id *IdentityKey) Public() []byte {
	cert, err := x509.MarshalPKIXPublicKey(&id.key.PublicKey)
	if err != nil {
		return nil
	}
	return cert
}

// NewIdentityKey builds a new identity key.
func NewIdentityKey() *IdentityKey {
	id := new(IdentityKey)

	var err error
	id.key, err = pks.GenerateKey()
	if err == nil {
		return id
	}
	return nil
}

// ImportPeerIdentity takes an exported public identity key and
// returns the appropriate RSA key.
func ImportPeerIdentity(in []byte) *rsa.PublicKey {
	if in == nil {
		return nil
	}
	cert, err := x509.ParsePKIXPublicKey(in)
	if err != nil {
		return nil
	}
	return cert.(*rsa.PublicKey)
}

// A SessionKey should be generated for each new session with a single peer.
type SessionKey struct {
	key       *dhkam.PrivateKey
	signedKey []byte
	peer      *dhkam.PublicKey
}

type signedDHKey struct {
	Public    []byte
	Signature []byte
}

// NewSessionKey builds a new session and returns it. Once this is
// returned, the Public() value should be sent to the peer, and
// once that Public() value is received, the peer should call
// PeerSessionKey before attempting to use the session key for
// encryption.
func (id *IdentityKey) NewSessionKey() *SessionKey {
	skey := new(SessionKey)

	var err error
	skey.key, err = dhkam.GenerateKey(PRNG)
	if err != nil {
		return nil
	}

	var sdhkey signedDHKey
	sdhkey.Public = skey.key.Export()
	sdhkey.Signature, err = pks.Sign(id.key, sdhkey.Public)
	if err != nil {
		return nil
	}

	skey.signedKey, err = asn1.Marshal(sdhkey)
	if err != nil {
		return nil
	}
	return skey
}

// Public should be used to export the signed public key to the client
func (skey *SessionKey) Public() []byte {
	return skey.signedKey
}

// PeerSessionKey reads the session key passed and checks the
// signature on it; if the signature is valid, it returns the peer's
// DH public key. On failure, it returns nil.
func (skey *SessionKey) PeerSessionKey(peer *rsa.PublicKey, session []byte) error {
	var signedKey signedDHKey
	_, err := asn1.Unmarshal(session, &signedKey)
	if err != nil {
		return err
	}

	if err = pks.Verify(peer, signedKey.Public, signedKey.Signature); err != nil {
		return err
	}

	pub, err := dhkam.ImportPublic(signedKey.Public)
	if err != nil {
		return err
	}
	skey.peer = pub
	return nil
}

// Decrypt takes the incoming ciphertext and decrypts it.
func (skey *SessionKey) Decrypt(in []byte) ([]byte, error) {
	var ephem struct {
		Pub []byte
		CT  []byte
	}

	_, err := asn1.Unmarshal(in, &ephem)
	if err != nil {
		return nil, err
	}

	pub, err := dhkam.ImportPublic(ephem.Pub)
	if err != nil {
		return nil, err
	}

	shared, err := skey.key.SharedKey(PRNG, pub, sharedKeyLen)
	if err != nil {
		return nil, err
	}

	symkey := shared[:authsym.SymKeyLen]
	mackey := shared[authsym.SymKeyLen:]
	out, err := authsym.Decrypt(symkey, mackey, ephem.CT)
	if err != nil {
		return nil, err
	}
	return out, nil
}

// Encrypt takes a message and encrypts it to the session's peer.
func (skey *SessionKey) Encrypt(message []byte) ([]byte, error) {
	dhEphem, err := dhkam.GenerateKey(PRNG)
	if err != nil {
		return nil, err
	}

	shared, err := dhEphem.SharedKey(PRNG, skey.peer, sharedKeyLen)
	if err != nil {
		return nil, err
	}

	var ephem struct {
		Pub []byte
		CT  []byte
	}

	symkey := shared[:authsym.SymKeyLen]
	mackey := shared[authsym.SymKeyLen:]
	ephem.CT, err = authsym.Encrypt(symkey, mackey, message)
	if err != nil {
		return nil, err
	}

	ephem.Pub = dhEphem.Export()
	return asn1.Marshal(ephem)
}
<|endoftext|>


<commit_before>// Copyright 2019 Istio 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 cmd

import (
	"istio.io/istio/tools/istio-iptables/pkg/constants"
	dep "istio.io/istio/tools/istio-iptables/pkg/dependencies"
)

func flushAndDeleteChains(ext dep.Dependencies, cmd string, table string, chains []string) {
	for _, chain := range chains {
		ext.RunQuietlyAndIgnore(cmd, "-t", table, "-F", chain)
		ext.RunQuietlyAndIgnore(cmd, "-t", table, "-X", chain)
	}
}

func removeOldChains(ext dep.Dependencies, cmd string) {
	for _, table := range []string{constants.NAT, constants.MANGLE} {
		// Remove the old chains
		ext.RunQuietlyAndIgnore(cmd, "-t", table, "-D", constants.PREROUTING, "-p", constants.TCP, "-j", constants.ISTIOINBOUND)
	}
	ext.RunQuietlyAndIgnore(cmd, "-t", constants.NAT, "-D", constants.OUTPUT, "-p", constants.TCP, "-j", constants.ISTIOOUTPUT)

	// Flush and delete the istio chains from NAT table.
	chains := []string{constants.ISTIOOUTPUT, constants.ISTIOINBOUND}
	flushAndDeleteChains(ext, cmd, constants.NAT, chains)
	// Flush and delete the istio chains from MANGLE table.
	chains = []string{constants.ISTIOINBOUND, constants.ISTIODIVERT, constants.ISTIOTPROXY}
	flushAndDeleteChains(ext, cmd, constants.MANGLE, chains)

	// Must be last, the others refer to it
	chains = []string{constants.ISTIOREDIRECT, constants.ISTIOINREDIRECT}
	flushAndDeleteChains(ext, cmd, constants.NAT, chains)
}

func cleanup(dryRun bool) {
	var ext dep.Dependencies
	if dryRun {
		ext = &dep.StdoutStubDependencies{}
	} else {
		ext = &dep.RealDependencies{}
	}

	defer func() {
		for _, cmd := range []string{dep.IPTABLESSAVE, dep.IP6TABLESSAVE} {
			ext.RunOrFail(cmd)
		}
	}()

	for _, cmd := range []string{dep.IPTABLES, dep.IP6TABLES} {
		removeOldChains(ext, cmd)
	}
}
<commit_msg>never fail iptables-save (#19716)<commit_after>// Copyright 2019 Istio 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 cmd

import (
	"istio.io/istio/tools/istio-iptables/pkg/constants"
	dep "istio.io/istio/tools/istio-iptables/pkg/dependencies"
)

func flushAndDeleteChains(ext dep.Dependencies, cmd string, table string, chains []string) {
	for _, chain := range chains {
		ext.RunQuietlyAndIgnore(cmd, "-t", table, "-F", chain)
		ext.RunQuietlyAndIgnore(cmd, "-t", table, "-X", chain)
	}
}

func removeOldChains(ext dep.Dependencies, cmd string) {
	for _, table := range []string{constants.NAT, constants.MANGLE} {
		// Remove the old chains
		ext.RunQuietlyAndIgnore(cmd, "-t", table, "-D", constants.PREROUTING, "-p", constants.TCP, "-j", constants.ISTIOINBOUND)
	}
	ext.RunQuietlyAndIgnore(cmd, "-t", constants.NAT, "-D", constants.OUTPUT, "-p", constants.TCP, "-j", constants.ISTIOOUTPUT)

	// Flush and delete the istio chains from NAT table.
	chains := []string{constants.ISTIOOUTPUT, constants.ISTIOINBOUND}
	flushAndDeleteChains(ext, cmd, constants.NAT, chains)
	// Flush and delete the istio chains from MANGLE table.
	chains = []string{constants.ISTIOINBOUND, constants.ISTIODIVERT, constants.ISTIOTPROXY}
	flushAndDeleteChains(ext, cmd, constants.MANGLE, chains)

	// Must be last, the others refer to it
	chains = []string{constants.ISTIOREDIRECT, constants.ISTIOINREDIRECT}
	flushAndDeleteChains(ext, cmd, constants.NAT, chains)
}

func cleanup(dryRun bool) {
	var ext dep.Dependencies
	if dryRun {
		ext = &dep.StdoutStubDependencies{}
	} else {
		ext = &dep.RealDependencies{}
	}

	defer func() {
		for _, cmd := range []string{dep.IPTABLESSAVE, dep.IP6TABLESSAVE} {
			// iptables-save is best efforts
			_ = ext.Run(cmd)
		}
	}()

	for _, cmd := range []string{dep.IPTABLES, dep.IP6TABLES} {
		removeOldChains(ext, cmd)
	}
}
<|endoftext|>


<commit_before>package client_test

import (
	. "github.com/bitpay/bitpay-go/client"
	ku "github.com/bitpay/bitpay-go/key_utils"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"os"
	"time"
)

var _ = Describe("RetrieveInvoice", func() {
	It("Retrieves an invoice from the server with an id", func() {
		time.Sleep(5)
		pm := ku.GeneratePem()
		apiuri := os.ExpandEnv("$RCROOTADDRESS")
		webClient := Client{ApiUri: apiuri, Insecure: true, Pem: pm}
		code := os.ExpandEnv("RETRIEVEPAIR")
		token, _ := webClient.PairWithCode(code)
		webClient.Token = token
		response, _ := webClient.CreateInvoice(10, "USD")
		invoiceId := response.Id
		retrievedInvoice, _ := webClient.GetInvoice(invoiceId)
		Expect(retrievedInvoice.Id).To(Equal(invoiceId))
	})
})
<commit_msg>checking for more errors<commit_after>package client_test

import (
	. "github.com/bitpay/bitpay-go/client"
	ku "github.com/bitpay/bitpay-go/key_utils"
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"os"
	"time"
)

var _ = Describe("RetrieveInvoice", func() {
	It("Retrieves an invoice from the server with an id", func() {
		time.Sleep(5)
		pm := ku.GeneratePem()
		apiuri := os.ExpandEnv("$RCROOTADDRESS")
		webClient := Client{ApiUri: apiuri, Insecure: true, Pem: pm}
		code := os.ExpandEnv("RETRIEVEPAIR")
		token, _ := webClient.PairWithCode(code)
		webClient.Token = token
		response, err:= webClient.CreateInvoice(10, "USD")
		if err != nil {
			println("the retrieve test errored while creating an invoice")
		invoiceId := response.Id
		retrievedInvoice, err := webClient.GetInvoice(invoiceId)
		if err != nil {
			println("The retrieve test errored while retrieving an invoice")
		Expect(retrievedInvoice.Id).To(Equal(invoiceId))
		Expect(retrievedInvoice.Price).To(Equal(response.Price))
	})
})
<|endoftext|>


<commit_before>// Copyright (c) 2017 Cisco and/or its affiliates.
//
// 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 defaultplugins

import (
	"context"

	intf "github.com/ligato/vpp-agent/plugins/defaultplugins/ifplugin/model/interfaces"
	"github.com/ligato/vpp-agent/plugins/defaultplugins/l2plugin/model/l2"
)

// Resync deletes obsolete operation status of network interfaces in DB.
// Obsolete state is one that is not part of SwIfIndex.
func (plugin *Plugin) resyncIfStateEvents(keys []string) error {
	for _, key := range keys {
		ifaceName, err := intf.ParseNameFromKey(key)
		if err != nil {
			return err
		}

		_, _, found := plugin.swIfIndexes.LookupIdx(ifaceName)
		if !found {
			err := plugin.PublishStatistics.Put(key, nil /*means delete*/)
			if err != nil {
				return err
			}
			plugin.Log.Debugf("Obsolete status for %v deleted", key)
		} else {
			plugin.Log.WithField("ifaceName", ifaceName).Debug("interface status is needed")
		}
	}

	return nil
}

// publishIfState goroutine is used to watch interface state notifications that are propagated to Messaging topic.
func (plugin *Plugin) publishIfStateEvents(ctx context.Context) {
	plugin.wg.Add(1)
	defer plugin.wg.Done()

	for {
		select {
		case ifState := <-plugin.ifStateChan:
			key := intf.InterfaceStateKey(ifState.State.Name)

			if plugin.PublishStatistics != nil {
				err := plugin.PublishStatistics.Put(key, ifState.State)
				if err != nil {
					plugin.Log.Error(err)
				} else {
					plugin.Log.Debug("Sending Messaging notification")
				}
			}

			// Marshall data into JSON & send kafka message.
			if plugin.ifStateNotifications != nil && ifState.Type == intf.UPDOWN {
				err := plugin.ifStateNotifications.Put(key, ifState.State)
				if err != nil {
					plugin.Log.Error(err)
				} else {
					plugin.Log.Debug("Sending Messaging notification")
				}
			}

		case <-ctx.Done():
			// Stop watching for state data updates.
			return
		}
	}
}

// Resync deletes old operation status of bridge domains in ETCD.
func (plugin *Plugin) resyncBdStateEvents(keys []string) error {
	for _, key := range keys {
		bdName, err := intf.ParseNameFromKey(key)
		if err != nil {
			return err
		}
		_, _, found := plugin.bdIndexes.LookupIdx(bdName)
		if !found {
			err := plugin.Publish.Put(key, nil)
			if err != nil {
				return err
			}
			plugin.Log.Debugf("Obsolete status for %v deleted", key)
		} else {
			plugin.Log.WithField("bdName", bdName).Debug("bridge domain status required")
		}
	}

	return nil
}

// PublishBdState is used to watch bridge domain state notifications.
func (plugin *Plugin) publishBdStateEvents(ctx context.Context) {
	plugin.wg.Add(1)
	defer plugin.wg.Done()

	for {
		select {
		case bdState := <-plugin.bdStateChan:
			if bdState != nil && bdState.State != nil && plugin.Publish != nil {
				key := l2.BridgeDomainStateKey(bdState.State.InternalName)
				// Remove BD state
				if bdState.State.Index == 0 && bdState.State.InternalName != "" {
					plugin.Publish.Put(key, nil)
					plugin.Log.Debugf("Bridge domain %v: state removed from ETCD", bdState.State.InternalName)
					// Write/Update BD state
				} else if bdState.State.Index != 0 {
					plugin.Publish.Put(key, bdState.State)
					plugin.Log.Debugf("Bridge domain %v: state stored in ETCD", bdState.State.InternalName)
				} else {
					plugin.Log.Warnf("Unable to process bridge domain state with Idx %v and Name %v",
						bdState.State.Index, bdState.State.InternalName)
				}
			}
		case <-ctx.Done():
			// Stop watching for state data updates.
			return
		}
	}
}
<commit_msg>Removing unnecessary debug message<commit_after>// Copyright (c) 2017 Cisco and/or its affiliates.
//
// 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 defaultplugins

import (
	"context"

	intf "github.com/ligato/vpp-agent/plugins/defaultplugins/ifplugin/model/interfaces"
	"github.com/ligato/vpp-agent/plugins/defaultplugins/l2plugin/model/l2"
)

// Resync deletes obsolete operation status of network interfaces in DB.
// Obsolete state is one that is not part of SwIfIndex.
func (plugin *Plugin) resyncIfStateEvents(keys []string) error {
	for _, key := range keys {
		ifaceName, err := intf.ParseNameFromKey(key)
		if err != nil {
			return err
		}

		_, _, found := plugin.swIfIndexes.LookupIdx(ifaceName)
		if !found {
			err := plugin.PublishStatistics.Put(key, nil /*means delete*/)
			if err != nil {
				return err
			}
			plugin.Log.Debugf("Obsolete status for %v deleted", key)
		} else {
			plugin.Log.WithField("ifaceName", ifaceName).Debug("interface status is needed")
		}
	}

	return nil
}

// publishIfState goroutine is used to watch interface state notifications that are propagated to Messaging topic.
func (plugin *Plugin) publishIfStateEvents(ctx context.Context) {
	plugin.wg.Add(1)
	defer plugin.wg.Done()

	for {
		select {
		case ifState := <-plugin.ifStateChan:
			key := intf.InterfaceStateKey(ifState.State.Name)

			if plugin.PublishStatistics != nil {
				err := plugin.PublishStatistics.Put(key, ifState.State)
				if err != nil {
					plugin.Log.Error(err)
				}
			}

			// Marshall data into JSON & send kafka message.
			if plugin.ifStateNotifications != nil && ifState.Type == intf.UPDOWN {
				err := plugin.ifStateNotifications.Put(key, ifState.State)
				if err != nil {
					plugin.Log.Error(err)
				}
			}

		case <-ctx.Done():
			// Stop watching for state data updates.
			return
		}
	}
}

// Resync deletes old operation status of bridge domains in ETCD.
func (plugin *Plugin) resyncBdStateEvents(keys []string) error {
	for _, key := range keys {
		bdName, err := intf.ParseNameFromKey(key)
		if err != nil {
			return err
		}
		_, _, found := plugin.bdIndexes.LookupIdx(bdName)
		if !found {
			err := plugin.Publish.Put(key, nil)
			if err != nil {
				return err
			}
			plugin.Log.Debugf("Obsolete status for %v deleted", key)
		} else {
			plugin.Log.WithField("bdName", bdName).Debug("bridge domain status required")
		}
	}

	return nil
}

// PublishBdState is used to watch bridge domain state notifications.
func (plugin *Plugin) publishBdStateEvents(ctx context.Context) {
	plugin.wg.Add(1)
	defer plugin.wg.Done()

	for {
		select {
		case bdState := <-plugin.bdStateChan:
			if bdState != nil && bdState.State != nil && plugin.Publish != nil {
				key := l2.BridgeDomainStateKey(bdState.State.InternalName)
				// Remove BD state
				if bdState.State.Index == 0 && bdState.State.InternalName != "" {
					plugin.Publish.Put(key, nil)
					plugin.Log.Debugf("Bridge domain %v: state removed from ETCD", bdState.State.InternalName)
					// Write/Update BD state
				} else if bdState.State.Index != 0 {
					plugin.Publish.Put(key, bdState.State)
					plugin.Log.Debugf("Bridge domain %v: state stored in ETCD", bdState.State.InternalName)
				} else {
					plugin.Log.Warnf("Unable to process bridge domain state with Idx %v and Name %v",
						bdState.State.Index, bdState.State.InternalName)
				}
			}
		case <-ctx.Done():
			// Stop watching for state data updates.
			return
		}
	}
}
<|endoftext|>


<commit_before>package google_calendar

/*
 * Copyright 2016 Albert P. Tobey <atobey@netflix.com>
 *
 * 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.
 */

import (
	"fmt"
	"log"
	"strconv"
	"sync"
	"time"

	"github.com/netflix/hal-9001/hal"
)

/* Even when attached, this plugin will not do anything until it is fully configured
 * for the room. At a mininum the calendar-id needs to be set. One or all of autoreply,
 * announce-start, and announce-end should be set to true to make anything happen.
 * Setting up:
 * !prefs set --room <roomid> --plugin google_calendar --key calendar-id --value <calendar link>
 *
 * autoreply: when set to true, the bot will reply with a message for any activity in the
 * room during hours when an event exists on the calendar. If the event has a description
 * set, that will be the text sent to the room. Otherwise a default message is generated.
 * !prefs set --room <roomid> --plugin google_calendar --key autoreply --value true
 *
 * announce-(start|end): the bot will automatically announce when an event is starting or
 * ending. The event's description will be included if it is not empty.
 * !prefs set --room <roomid> --plugin google_calendar --key announce-start --value true
 * !prefs set --room <roomid> --plugin google_calendar --key announce-end --value true
 *
 * timezone: optional, tells the bot which timezone to report dates in
 * !prefs set --room <roomid> --plugin google_calendar --key timezone --value America/Los_Angeles
 */

const DefaultTz = "America/Los_Angeles"
const DefaultMsg = "Calendar event: %q"

type Config struct {
	RoomId        string
	CalendarId    string
	Timezone      time.Location
	Autoreply     bool
	AnnounceStart bool
	AnnounceEnd   bool
	CalEvents     []CalEvent
	LastReply     time.Time
	mut           sync.Mutex
	configTs      time.Time
	calTs         time.Time
}

var configCache map[string]*Config
var topMut sync.Mutex

func init() {
	configCache = make(map[string]*Config)
}

func Register() {
	p := hal.Plugin{
		Name: "google_calendar",
		Func: handleEvt,
		Init: initData,
	}

	p.Register()
}

// initData primes the cache and starts the background goroutine
func initData(inst *hal.Instance) {
	topMut.Lock()
	config := Config{RoomId: inst.RoomId}
	configCache[inst.RoomId] = &config
	topMut.Unlock()

	// initiate the loading of events
	config.getCachedCalEvents(time.Now())

	// TODO: kick off background refresh
}

// handleEvt handles events coming in from the chat system. It does not interact
// directly with the calendar API and relies on the background goroutine to populate
// the cache.
func handleEvt(evt hal.Evt) {
	now := time.Now()
	config := getCachedConfig(evt.RoomId, now)
	calEvents, err := config.getCachedCalEvents(now)
	if err != nil {
		evt.Replyf("Error while getting calendar data: %s", err)
		return
	}

	for _, e := range calEvents {
		if config.Autoreply && e.Start.Before(now) && e.End.After(now) {
			lastReplyAge := now.Sub(config.LastReply)
			// TODO: track more detailed state to make squelching replies easier
			// for now: only reply once an hour
			if lastReplyAge.Hours() < 1 {
				log.Printf("not autoresponding because a message has been sent in the last hour")
				continue
			}

			if e.Description != "" {
				evt.Reply(e.Description)
			} else {
				evt.Replyf(DefaultMsg, e.Name)
			}

			config.LastReply = now
			// return // TODO: should overlapping events mean multiple messages?
		}
	}
}

// TODO: announce start / end

func getCachedConfig(roomId string, now time.Time) Config {
	topMut.Lock()
	c := configCache[roomId]
	topMut.Unlock()

	age := now.Sub(c.configTs)

	if age.Minutes() > 10 {
		c.LoadFromPrefs()
	}

	return *c
}

// getCachedEvents fetches the calendar data from the Google Calendar API,
// holding a mutex while doing so. This prevents handleEvt from firing until
// the first load of data is complete and will block the goroutines for a short
// time.
func (c *Config) getCachedCalEvents(now time.Time) ([]CalEvent, error) {
	c.mut.Lock()
	defer c.mut.Unlock()

	calAge := now.Sub(c.calTs)

	if calAge.Hours() > 1.5 {
		evts, err := getEvents(c.CalendarId, now)
		if err != nil {
			return nil, err
		} else {
			c.CalEvents = evts
		}
	}

	return c.CalEvents, nil
}

func (c *Config) LoadFromPrefs() error {
	c.mut.Lock()
	defer c.mut.Unlock()

	cidpref := hal.GetPref("", "", c.RoomId, "google_calendar", "calendar-id", "")
	if cidpref.Success {
		c.CalendarId = cidpref.Value
	} else {
		return fmt.Errorf("Failed to load calendar-id preference for room %q: %s", c.RoomId, cidpref.Error)
	}

	c.Autoreply = c.loadBoolPref("autoreply")
	c.AnnounceStart = c.loadBoolPref("announce-start")
	c.AnnounceEnd = c.loadBoolPref("announce-end")

	tzpref := hal.GetPref("", "", c.RoomId, "google_calendar", "timezone", DefaultTz)
	tz, err := time.LoadLocation(tzpref.Value)
	if err != nil {
		return fmt.Errorf("Could not load timezone info for '%s': %s\n", tzpref.Value, err)
	}
	c.Timezone = *tz

	c.configTs = time.Now()

	return nil
}

func (c *Config) loadBoolPref(key string) bool {
	pref := hal.GetPref("", "", c.RoomId, "google_calendar", key, "false")

	val, err := strconv.ParseBool(pref.Value)
	if err != nil {
		log.Printf("unable to parse boolean pref value: %s", err)
		return false
	}

	return val
}
<commit_msg>fix bugs & add admin commands<commit_after>package google_calendar

/*
 * Copyright 2016 Albert P. Tobey <atobey@netflix.com>
 *
 * 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.
 */

// TODO: announce start / end

import (
	"fmt"
	"log"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/netflix/hal-9001/hal"
)

const Usage = `
Even when attached, this plugin will not do anything until it is fully configured
for the room. At a mininum the calendar-id needs to be set. One or all of autoreply,
announce-start, and announce-end should be set to true to make anything happen.

Setting up:

    !prefs set --room <roomid> --plugin google_calendar --key calendar-id --value <calendar link>

    autoreply: when set to true, the bot will reply with a message for any activity in the
    room during hours when an event exists on the calendar. If the event has a description
    set, that will be the text sent to the room. Otherwise a default message is generated.
    !prefs set --room <roomid> --plugin google_calendar --key autoreply --value true

    announce-(start|end): the bot will automatically announce when an event is starting or
    ending. The event's description will be included if it is not empty.
    !prefs set --room <roomid> --plugin google_calendar --key announce-start --value true
    !prefs set --room <roomid> --plugin google_calendar --key announce-end --value true

    timezone: optional, tells the bot which timezone to report dates in
    !prefs set --room <roomid> --plugin google_calendar --key timezone --value America/Los_Angeles
`

const DefaultTz = "America/Los_Angeles"
const DefaultMsg = "Calendar event: %q"

type Config struct {
	RoomId        string
	CalendarId    string
	Timezone      time.Location
	Autoreply     bool
	AnnounceStart bool
	AnnounceEnd   bool
	CalEvents     []CalEvent
	LastReply     time.Time
	mut           sync.Mutex
	configTs      time.Time
	calTs         time.Time
}

var configCache map[string]*Config
var topMut sync.Mutex

func init() {
	configCache = make(map[string]*Config)
}

func Register() {
	p := hal.Plugin{
		Name: "google_calendar",
		Func: handleEvt,
		Init: initData,
	}

	p.Register()
}

// initData primes the cache and starts the background goroutine
func initData(inst *hal.Instance) {
	topMut.Lock()
	config := Config{RoomId: inst.RoomId}
	configCache[inst.RoomId] = &config
	topMut.Unlock()

	go func() {
		// wait one minute before kicking off the background refresh
		time.Sleep(time.Minute)

		pf := hal.PeriodicFunc{
			Name:     "google_calendar-" + inst.RoomId,
			Interval: time.Minute * 10,
			Function: func() { updateCachedCalEvents(inst.RoomId) },
		}
		pf.Register()
	}()
}

// handleEvt handles events coming in from the chat system. It does not interact
// directly with the calendar API and relies on the background goroutine to populate
// the cache.
func handleEvt(evt hal.Evt) {
	if strings.HasPrefix(strings.TrimSpace(evt.Body), "!") {
		handleCommand(&evt)
		return
	}

	now := time.Now()
	config := getCachedConfig(evt.RoomId, now)
	calEvents, err := config.getCachedCalEvents(now)
	if err != nil {
		evt.Replyf("Error while getting calendar data: %s", err)
		return
	}

	for _, e := range calEvents {
		if config.Autoreply && e.Start.Before(now) && e.End.After(now) {
			lastReplyAge := now.Sub(config.LastReply)
			// TODO: track more detailed state to make squelching replies easier
			// for now: only reply once an hour
			if lastReplyAge.Hours() < 1 {
				log.Printf("not autoresponding because a message has been sent in the last hour")
				continue
			}

			if e.Description != "" {
				evt.Reply(e.Description)
			} else {
				evt.Replyf(DefaultMsg, e.Name)
			}

			config.LastReply = now
			// return // TODO: should overlapping events mean multiple messages?
		}
	}
}

func handleCommand(evt *hal.Evt) {
	argv := evt.BodyAsArgv()

	if argv[0] != "!gcal" {
		return
	}

	if len(argv) < 2 {
		evt.Replyf(Usage)
		return
	}

	now := time.Now()
	config := getCachedConfig(evt.RoomId, now)

	switch argv[1] {
	case "status":
		evt.Replyf("Calendar cache is %.f minutes old. Config cache is %.f minutes old.",
			now.Sub(config.calTs).Minutes(), now.Sub(config.configTs).Minutes())
	case "help":
		evt.Replyf(Usage)
	case "expire":
		config.expireCaches()
		evt.Replyf("config & calendar caches expired")
	case "reload":
		config.expireCaches()
		updateCachedCalEvents(evt.RoomId)
		evt.Replyf("reload complete")
	}
}

func updateCachedCalEvents(roomId string) {
	log.Printf("START: updateCachedCalEvents(%q)", roomId)
	topMut.Lock()
	defer topMut.Unlock()

	now := time.Now()

	c := configCache[roomId]

	// update the config from prefs
	c.LoadFromPrefs()

	// force-expire the cache
	c.calTs = now.Add(time.Hour * -2)

	_, err := c.getCachedCalEvents(now)
	if err != nil {
		log.Printf("FAILED: updateCachedCalEvents(%q): %s", roomId, err)
	}

	log.Printf("DONE: updateCachedCalEvents(%q) @ %s", roomId, now)
}

func getCachedConfig(roomId string, now time.Time) *Config {
	topMut.Lock()
	c := configCache[roomId]
	topMut.Unlock()

	age := now.Sub(c.configTs)

	if age.Minutes() > 10 {
		c.LoadFromPrefs()
	}

	return c
}

// getCachedEvents fetches the calendar data from the Google Calendar API,
// holding a mutex while doing so. This prevents handleEvt from firing until
// the first load of data is complete and will block the goroutines for a short
// time.
func (c *Config) getCachedCalEvents(now time.Time) ([]CalEvent, error) {
	c.mut.Lock()
	defer c.mut.Unlock()

	calAge := now.Sub(c.calTs)

	if calAge.Hours() > 1.1 {
		log.Printf("%q's calendar cache appears to be expired after %f hours", c.RoomId, calAge.Hours())
		evts, err := getEvents(c.CalendarId, now)
		if err != nil {
			log.Printf("Error encountered while fetching calendar events: %s", err)
			return nil, err
		} else {
			c.calTs = now
			c.CalEvents = evts
		}
	}

	return c.CalEvents, nil
}

func (c *Config) LoadFromPrefs() error {
	c.mut.Lock()
	defer c.mut.Unlock()

	cidpref := hal.GetPref("", "", c.RoomId, "google_calendar", "calendar-id", "")
	if cidpref.Success {
		c.CalendarId = cidpref.Value
	} else {
		return fmt.Errorf("Failed to load calendar-id preference for room %q: %s", c.RoomId, cidpref.Error)
	}

	c.Autoreply = c.loadBoolPref("autoreply")
	c.AnnounceStart = c.loadBoolPref("announce-start")
	c.AnnounceEnd = c.loadBoolPref("announce-end")

	tzpref := hal.GetPref("", "", c.RoomId, "google_calendar", "timezone", DefaultTz)
	tz, err := time.LoadLocation(tzpref.Value)
	if err != nil {
		return fmt.Errorf("Could not load timezone info for '%s': %s\n", tzpref.Value, err)
	}
	c.Timezone = *tz

	c.configTs = time.Now()

	return nil
}

func (c *Config) expireCaches() {
	c.calTs = time.Time{}
	c.configTs = time.Time{}
}

func (c *Config) loadBoolPref(key string) bool {
	pref := hal.GetPref("", "", c.RoomId, "google_calendar", key, "false")

	val, err := strconv.ParseBool(pref.Value)
	if err != nil {
		log.Printf("unable to parse boolean pref value: %s", err)
		return false
	}

	return val
}
<|endoftext|>