<commit_before>package connector

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"strings"
	"time"

	"github.com/facette/facette/pkg/catalog"
	"github.com/facette/facette/pkg/config"
	"github.com/facette/facette/pkg/library"
	"github.com/facette/facette/pkg/types"
	"github.com/facette/facette/pkg/utils"
)

const (
	facetteURLCatalog            string = "/api/v1/catalog/"
	facetteURLLibraryGraphsPlots string = "/api/v1/library/graphs/plots"
)

// FacetteConnector represents the main structure of the Facette connector.
type FacetteConnector struct {
	upstream string
}

func init() {
	Connectors["facette"] = func(settings map[string]interface{}) (Connector, error) {
		var err error

		connector := &FacetteConnector{}

		if connector.upstream, err = config.GetString(settings, "upstream", true); err != nil {
			return nil, err
		}

		return connector, nil
	}
}

// GetPlots retrieves time series data from origin based on a query and a time interval.
func (connector *FacetteConnector) GetPlots(query *types.PlotQuery) (map[string]*types.PlotResult, error) {
	result := make(map[string]*types.PlotResult)

	// Convert plotQuery into plotRequest to forward query to upstream Facette API
	plotRequest := struct {
		Time        time.Time     `json:"time"`
		Range       string        `json:"range"`
		Sample      int           `json:"sample"`
		Percentiles []float64     `json:"percentiles"`
		Graph       library.Graph `json:"graph"`
	}{
		Time:  query.StartTime,
		Range: utils.DurationToRange(query.EndTime.Sub(query.StartTime)),
		Graph: library.Graph{
			Item: library.Item{
				Name: "facette",
			},
			Groups: []*library.OperGroup{
				&library.OperGroup{
					Name: query.Group.Name,
					Type: query.Group.Type,
					Series: func(series []*types.SerieQuery) []*library.Serie {
						requestSeries := make([]*library.Serie, len(series))

						for i, serie := range series {
							requestSeries[i] = &library.Serie{
								Name:   serie.Name,
								Origin: serie.Metric.Origin,
								Source: serie.Metric.Source,
								Metric: serie.Metric.Name,
								Scale:  serie.Scale,
							}
						}

						return requestSeries
					}(query.Group.Series),
					Scale: query.Group.Scale,
				},
			},
		},
	}

	if query.Step != 0 {
		plotRequest.Sample = int((query.EndTime.Sub(query.StartTime) / query.Step).Seconds())
	}

	requestBody, err := json.Marshal(plotRequest)
	if err != nil {
		return nil, fmt.Errorf("unable to marshal plot request: %s", err)
	}

	log.Printf("DEBUG: facetteConnector: >>> %s", requestBody)

	httpClient := http.Client{}

	request, err := http.NewRequest(
		"POST",
		strings.TrimSuffix(connector.upstream, "/")+facetteURLLibraryGraphsPlots,
		bytes.NewReader(requestBody))
	if err != nil {
		return nil, fmt.Errorf("unable to set up HTTP request: %s", err)
	}

	request.Header.Add("Content-Type", "application/json")
	request.Header.Add("User-Agent", "Facette")
	request.Header.Add("X-Requested-With", "FacetteConnector")

	response, err := httpClient.Do(request)
	if err != nil {
		return nil, err
	}

	if err := facetteCheckConnectorResponse(response); err != nil {
		return nil, fmt.Errorf("invalid upstream HTTP response: %s", err)
	}

	data, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return nil, fmt.Errorf("unable to read HTTP response body: %s", err)
	}

	log.Printf("DEBUG: facetteConnector: <<< %s", data)

	// result["blah"] = &types.PlotResult{
	// 	Plots: []types.PlotValue{42.0},
	// }

	return result, nil
}

// Refresh triggers a full connector data update.
func (connector *FacetteConnector) Refresh(originName string, outputChan chan *catalog.CatalogRecord) error {
	httpTransport := &http.Transport{
		Dial: (&net.Dialer{
			// Enable dual IPv4/IPv6 stack connectivity:
			DualStack: true,
		}).Dial,
	}

	httpClient := http.Client{Transport: httpTransport}

	response, err := httpClient.Get(strings.TrimSuffix(connector.upstream, "/") + facetteURLCatalog)
	if err != nil {
		return err
	}

	if err = facetteCheckConnectorResponse(response); err != nil {
		return fmt.Errorf("invalid HTTP backend response: %s", err)
	}

	data, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return fmt.Errorf("unable to read HTTP response body: %s", err)
	}

	upstreamCatalog := make(map[string]map[string][]string)
	if err = json.Unmarshal(data, &upstreamCatalog); err != nil {
		return fmt.Errorf("unable to unmarshal JSON data: %s", err)
	}

	// Parse the upstream catalog entries and append them to our local catalog
	for upstreamOriginName, upstreamOrigin := range upstreamCatalog {
		for sourceName, metrics := range upstreamOrigin {
			for _, metric := range metrics {
				outputChan <- &catalog.CatalogRecord{
					Origin:    upstreamOriginName,
					Source:    sourceName,
					Metric:    metric,
					Connector: connector,
				}
			}
		}
	}

	return nil
}

func facetteCheckConnectorResponse(response *http.Response) error {
	if response.StatusCode != 200 {
		return fmt.Errorf("got HTTP status code %d, expected 200", response.StatusCode)
	}

	if !strings.Contains(response.Header.Get("Content-Type"), "application/json") {
		return fmt.Errorf("got HTTP content type `%s', expected `application/json'", response.Header["Content-Type"])
	}

	return nil
}
<commit_msg>Facette connector returns upstream plots results<commit_after>package connector

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"strings"
	"time"

	"github.com/facette/facette/pkg/catalog"
	"github.com/facette/facette/pkg/config"
	"github.com/facette/facette/pkg/library"
	"github.com/facette/facette/pkg/types"
	"github.com/facette/facette/pkg/utils"
)

type facettePlotRequest struct {
	Time        time.Time     `json:"time"`
	Range       string        `json:"range"`
	Sample      int           `json:"sample"`
	Percentiles []float64     `json:"percentiles"`
	Graph       library.Graph `json:"graph"`
}

type facettePlotResponse struct {
	ID          string          `json:"id"`
	Start       string          `json:"start"`
	End         string          `json:"end"`
	Step        float64         `json:"step"`
	Name        string          `json:"name"`
	Description string          `json:"description"`
	Type        int             `json:"type"`
	StackMode   int             `json:"stack_mode"`
	Series      []*facetteSerie `json:"series"`
	Modified    time.Time       `json:"modified"`
}

type facetteSerie struct {
	Name    string                     `json:"name"`
	StackID int                        `json:"stack_id"`
	Plots   []types.PlotValue          `json:"plots"`
	Info    map[string]types.PlotValue `json:"info"`
	Options map[string]interface{}     `json:"options"`
}

// FacetteConnector represents the main structure of the Facette connector.
type FacetteConnector struct {
	upstream string
}

const (
	facetteURLCatalog            string = "/api/v1/catalog/"
	facetteURLLibraryGraphsPlots string = "/api/v1/library/graphs/plots"
)

func init() {
	Connectors["facette"] = func(settings map[string]interface{}) (Connector, error) {
		var err error

		connector := &FacetteConnector{}

		if connector.upstream, err = config.GetString(settings, "upstream", true); err != nil {
			return nil, err
		}

		return connector, nil
	}
}

// GetPlots retrieves time series data from origin based on a query and a time interval.
func (connector *FacetteConnector) GetPlots(query *types.PlotQuery) (map[string]*types.PlotResult, error) {
	// Convert plotQuery into plotRequest-like to forward query to upstream Facette API
	plotRequest := facettePlotRequest{
		Time:  query.StartTime,
		Range: utils.DurationToRange(query.EndTime.Sub(query.StartTime)),
		Graph: library.Graph{
			Item: library.Item{
				Name: "facette",
			},
			Groups: []*library.OperGroup{
				&library.OperGroup{
					Name: query.Group.Name,
					Type: query.Group.Type,
					Series: func(series []*types.SerieQuery) []*library.Serie {
						requestSeries := make([]*library.Serie, len(series))

						for i, serie := range series {
							requestSeries[i] = &library.Serie{
								Name:   serie.Name,
								Origin: serie.Metric.Origin,
								Source: serie.Metric.Source,
								Metric: serie.Metric.Name,
								Scale:  serie.Scale,
							}
						}

						return requestSeries
					}(query.Group.Series),
					Scale: query.Group.Scale,
				},
			},
		},
	}

	if query.Step != 0 {
		plotRequest.Sample = int((query.EndTime.Sub(query.StartTime) / query.Step).Seconds())
	}

	requestBody, err := json.Marshal(plotRequest)
	if err != nil {
		return nil, fmt.Errorf("unable to marshal plot request: %s", err)
	}

	log.Printf("DEBUG: facetteConnector: >>> %s", requestBody)

	httpClient := http.Client{}

	request, err := http.NewRequest(
		"POST",
		strings.TrimSuffix(connector.upstream, "/")+facetteURLLibraryGraphsPlots,
		bytes.NewReader(requestBody))
	if err != nil {
		return nil, fmt.Errorf("unable to set up HTTP request: %s", err)
	}

	request.Header.Add("Content-Type", "application/json")
	request.Header.Add("User-Agent", "Facette")
	request.Header.Add("X-Requested-With", "FacetteConnector")

	response, err := httpClient.Do(request)
	if err != nil {
		return nil, err
	}

	if err := facetteCheckConnectorResponse(response); err != nil {
		return nil, fmt.Errorf("invalid upstream HTTP response: %s", err)
	}

	data, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return nil, fmt.Errorf("unable to read HTTP response body: %s", err)
	}

	log.Printf("DEBUG: facetteConnector: <<< %s", data)

	plotResponse := facettePlotResponse{}

	if err := json.Unmarshal(data, &plotResponse); err != nil {
		return nil, fmt.Errorf("unable to unmarshal upstream response: %s", err)
	}

	result := make(map[string]*types.PlotResult)

	for _, serie := range plotResponse.Series {
		result[serie.Name] = &types.PlotResult{
			Plots: serie.Plots,
			Info:  serie.Info,
		}
	}

	return result, nil
}

// Refresh triggers a full connector data update.
func (connector *FacetteConnector) Refresh(originName string, outputChan chan *catalog.CatalogRecord) error {
	httpTransport := &http.Transport{
		Dial: (&net.Dialer{
			// Enable dual IPv4/IPv6 stack connectivity:
			DualStack: true,
		}).Dial,
	}

	httpClient := http.Client{Transport: httpTransport}

	response, err := httpClient.Get(strings.TrimSuffix(connector.upstream, "/") + facetteURLCatalog)
	if err != nil {
		return err
	}

	if err = facetteCheckConnectorResponse(response); err != nil {
		return fmt.Errorf("invalid HTTP backend response: %s", err)
	}

	data, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return fmt.Errorf("unable to read HTTP response body: %s", err)
	}

	upstreamCatalog := make(map[string]map[string][]string)
	if err = json.Unmarshal(data, &upstreamCatalog); err != nil {
		return fmt.Errorf("unable to unmarshal JSON data: %s", err)
	}

	// Parse the upstream catalog entries and append them to our local catalog
	for upstreamOriginName, upstreamOrigin := range upstreamCatalog {
		for sourceName, metrics := range upstreamOrigin {
			for _, metric := range metrics {
				outputChan <- &catalog.CatalogRecord{
					Origin:    upstreamOriginName,
					Source:    sourceName,
					Metric:    metric,
					Connector: connector,
				}
			}
		}
	}

	return nil
}

func facetteCheckConnectorResponse(response *http.Response) error {
	if response.StatusCode != 200 {
		return fmt.Errorf("got HTTP status code %d, expected 200", response.StatusCode)
	}

	if !strings.Contains(response.Header.Get("Content-Type"), "application/json") {
		return fmt.Errorf("got HTTP content type `%s', expected `application/json'", response.Header["Content-Type"])
	}

	return nil
}
<|endoftext|>


<commit_before>/*
Copyright 2014 The Kubernetes Authors All rights reserved.

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 conversion

import (
	"encoding/json"
	"errors"
	"fmt"
)

func (s *Scheme) DecodeToVersionedObject(data []byte) (obj interface{}, version, kind string, err error) {
	version, kind, err = s.DataVersionAndKind(data)
	if err != nil {
		return
	}
	if version == "" && s.InternalVersion != "" {
		return nil, "", "", fmt.Errorf("version not set in '%s'", string(data))
	}
	if kind == "" {
		return nil, "", "", fmt.Errorf("kind not set in '%s'", string(data))
	}
	obj, err = s.NewObject(version, kind)
	if err != nil {
		return nil, "", "", err
	}

	if err := json.Unmarshal(data, obj); err != nil {
		return nil, "", "", err
	}
	return
}

// Decode converts a JSON string back into a pointer to an api object.
// Deduces the type based upon the fields added by the MetaInsertionFactory
// technique. The object will be converted, if necessary, into the
// s.InternalVersion type before being returned. Decode will not decode
// objects without version set unless InternalVersion is also "".
func (s *Scheme) Decode(data []byte) (interface{}, error) {
	return s.DecodeToVersion(data, s.InternalVersion)
}

// DecodeToVersion converts a JSON string back into a pointer to an api object.
// Deduces the type based upon the fields added by the MetaInsertionFactory
// technique. The object will be converted, if necessary, into the versioned
// type before being returned. Decode will not decode objects without version
// set unless version is also "".
func (s *Scheme) DecodeToVersion(data []byte, version string) (interface{}, error) {
	obj, sourceVersion, kind, err := s.DecodeToVersionedObject(data)
	if err != nil {
		return nil, err
	}
	// Version and Kind should be blank in memory.
	if err := s.SetVersionAndKind("", "", obj); err != nil {
		return nil, err
	}

	// Convert if needed.
	if version != sourceVersion {
		objOut, err := s.NewObject(version, kind)
		if err != nil {
			return nil, err
		}
		flags, meta := s.generateConvertMeta(sourceVersion, version, obj)
		if err := s.converter.Convert(obj, objOut, flags, meta); err != nil {
			return nil, err
		}
		obj = objOut
	}
	return obj, nil
}

// DecodeInto parses a JSON string and stores it in obj. Returns an error
// if data.Kind is set and doesn't match the type of obj. Obj should be a
// pointer to an api type.
// If obj's version doesn't match that in data, an attempt will be made to convert
// data into obj's version.
func (s *Scheme) DecodeInto(data []byte, obj interface{}) error {
	return s.DecodeIntoWithSpecifiedVersionKind(data, obj, "", "")
}

// DecodeIntoWithSpecifiedVersionKind compares the passed in specifiedVersion and
// specifiedKind with data.Version and data.Kind, defaulting data.Version and
// data.Kind to the specified value if they are empty, or generating an error if
// data.Version and data.Kind are not empty and differ from the specified value.
// The function then implements the functionality of DecodeInto.
// If specifiedVersion and specifiedKind are empty, the function degenerates to
// DecodeInto.
func (s *Scheme) DecodeIntoWithSpecifiedVersionKind(data []byte, obj interface{}, specifiedVersion, specifiedKind string) error {
	if len(data) == 0 {
		return errors.New("empty input")
	}
	dataVersion, dataKind, err := s.DataVersionAndKind(data)
	if err != nil {
		return err
	}
	if dataVersion == "" {
		dataVersion = specifiedVersion
	}
	if dataKind == "" {
		dataKind = specifiedKind
	}
	if len(specifiedVersion) > 0 && (dataVersion != specifiedVersion) {
		return errors.New(fmt.Sprintf("The apiVersion in the data (%s) does not match the specified apiVersion(%s)", dataVersion, specifiedVersion))
	}
	if len(specifiedKind) > 0 && (dataKind != specifiedKind) {
		return errors.New(fmt.Sprintf("The kind in the data (%s) does not match the specified kind(%s)", dataKind, specifiedKind))
	}

	objVersion, objKind, err := s.ObjectVersionAndKind(obj)
	if err != nil {
		return err
	}
	if dataKind == "" {
		// Assume objects with unset Kind fields are being unmarshalled into the
		// correct type.
		dataKind = objKind
	}
	if dataVersion == "" {
		// Assume objects with unset Version fields are being unmarshalled into the
		// correct type.
		dataVersion = objVersion
	}

	external, err := s.NewObject(dataVersion, dataKind)
	if err != nil {
		return err
	}
	if err := json.Unmarshal(data, external); err != nil {
		return err
	}
	flags, meta := s.generateConvertMeta(dataVersion, objVersion, external)
	if err := s.converter.Convert(external, obj, flags, meta); err != nil {
		return err
	}

	// Version and Kind should be blank in memory.
	return s.SetVersionAndKind("", "", obj)
}
<commit_msg>Use ugorji/go/codec for decoding jsons.<commit_after>/*
Copyright 2014 The Kubernetes Authors All rights reserved.

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 conversion

import (
	"errors"
	"fmt"
	"github.com/ugorji/go/codec"
)

func (s *Scheme) DecodeToVersionedObject(data []byte) (obj interface{}, version, kind string, err error) {
	version, kind, err = s.DataVersionAndKind(data)
	if err != nil {
		return
	}
	if version == "" && s.InternalVersion != "" {
		return nil, "", "", fmt.Errorf("version not set in '%s'", string(data))
	}
	if kind == "" {
		return nil, "", "", fmt.Errorf("kind not set in '%s'", string(data))
	}
	obj, err = s.NewObject(version, kind)
	if err != nil {
		return nil, "", "", err
	}

	if err := codec.NewDecoderBytes(data, new(codec.JsonHandle)).Decode(obj); err != nil {
		return nil, "", "", err
	}
	return
}

// Decode converts a JSON string back into a pointer to an api object.
// Deduces the type based upon the fields added by the MetaInsertionFactory
// technique. The object will be converted, if necessary, into the
// s.InternalVersion type before being returned. Decode will not decode
// objects without version set unless InternalVersion is also "".
func (s *Scheme) Decode(data []byte) (interface{}, error) {
	return s.DecodeToVersion(data, s.InternalVersion)
}

// DecodeToVersion converts a JSON string back into a pointer to an api object.
// Deduces the type based upon the fields added by the MetaInsertionFactory
// technique. The object will be converted, if necessary, into the versioned
// type before being returned. Decode will not decode objects without version
// set unless version is also "".
func (s *Scheme) DecodeToVersion(data []byte, version string) (interface{}, error) {
	obj, sourceVersion, kind, err := s.DecodeToVersionedObject(data)
	if err != nil {
		return nil, err
	}
	// Version and Kind should be blank in memory.
	if err := s.SetVersionAndKind("", "", obj); err != nil {
		return nil, err
	}

	// Convert if needed.
	if version != sourceVersion {
		objOut, err := s.NewObject(version, kind)
		if err != nil {
			return nil, err
		}
		flags, meta := s.generateConvertMeta(sourceVersion, version, obj)
		if err := s.converter.Convert(obj, objOut, flags, meta); err != nil {
			return nil, err
		}
		obj = objOut
	}
	return obj, nil
}

// DecodeInto parses a JSON string and stores it in obj. Returns an error
// if data.Kind is set and doesn't match the type of obj. Obj should be a
// pointer to an api type.
// If obj's version doesn't match that in data, an attempt will be made to convert
// data into obj's version.
func (s *Scheme) DecodeInto(data []byte, obj interface{}) error {
	return s.DecodeIntoWithSpecifiedVersionKind(data, obj, "", "")
}

// DecodeIntoWithSpecifiedVersionKind compares the passed in specifiedVersion and
// specifiedKind with data.Version and data.Kind, defaulting data.Version and
// data.Kind to the specified value if they are empty, or generating an error if
// data.Version and data.Kind are not empty and differ from the specified value.
// The function then implements the functionality of DecodeInto.
// If specifiedVersion and specifiedKind are empty, the function degenerates to
// DecodeInto.
func (s *Scheme) DecodeIntoWithSpecifiedVersionKind(data []byte, obj interface{}, specifiedVersion, specifiedKind string) error {
	if len(data) == 0 {
		return errors.New("empty input")
	}
	dataVersion, dataKind, err := s.DataVersionAndKind(data)
	if err != nil {
		return err
	}
	if dataVersion == "" {
		dataVersion = specifiedVersion
	}
	if dataKind == "" {
		dataKind = specifiedKind
	}
	if len(specifiedVersion) > 0 && (dataVersion != specifiedVersion) {
		return errors.New(fmt.Sprintf("The apiVersion in the data (%s) does not match the specified apiVersion(%s)", dataVersion, specifiedVersion))
	}
	if len(specifiedKind) > 0 && (dataKind != specifiedKind) {
		return errors.New(fmt.Sprintf("The kind in the data (%s) does not match the specified kind(%s)", dataKind, specifiedKind))
	}

	objVersion, objKind, err := s.ObjectVersionAndKind(obj)
	if err != nil {
		return err
	}
	if dataKind == "" {
		// Assume objects with unset Kind fields are being unmarshalled into the
		// correct type.
		dataKind = objKind
	}
	if dataVersion == "" {
		// Assume objects with unset Version fields are being unmarshalled into the
		// correct type.
		dataVersion = objVersion
	}

	external, err := s.NewObject(dataVersion, dataKind)
	if err != nil {
		return err
	}
	if err := codec.NewDecoderBytes(data, new(codec.JsonHandle)).Decode(external); err != nil {
		return err
	}
	flags, meta := s.generateConvertMeta(dataVersion, objVersion, external)
	if err := s.converter.Convert(external, obj, flags, meta); err != nil {
		return err
	}

	// Version and Kind should be blank in memory.
	return s.SetVersionAndKind("", "", obj)
}
<|endoftext|>


<commit_before>/*
Copyright 2014 Google Inc. All rights reserved.

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 conversion

import (
	"fmt"
	"reflect"
)

// Scheme defines an entire encoding and decoding scheme.
type Scheme struct {
	// versionMap allows one to figure out the go type of an object with
	// the given version and name.
	versionMap map[string]map[string]reflect.Type

	// typeToVersion allows one to figure out the version for a given go object.
	// The reflect.Type we index by should *not* be a pointer. If the same type
	// is registered for multiple versions, the last one wins.
	typeToVersion map[reflect.Type]string

	// typeToKind allows one to figure out the desired "kind" field for a given
	// go object. Requirements and caveats are the same as typeToVersion.
	typeToKind map[reflect.Type][]string

	// converter stores all registered conversion functions. It also has
	// default coverting behavior.
	converter *Converter

	// Indent will cause the JSON output from Encode to be indented, iff it is true.
	Indent bool

	// InternalVersion is the default internal version. It is recommended that
	// you use "" for the internal version.
	InternalVersion string

	// MetaInsertionFactory is used to create an object to store and retrieve
	// the version and kind information for all objects. The default uses the
	// keys "apiVersion" and "kind" respectively.
	MetaFactory MetaFactory
}

// NewScheme manufactures a new scheme.
func NewScheme() *Scheme {
	s := &Scheme{
		versionMap:      map[string]map[string]reflect.Type{},
		typeToVersion:   map[reflect.Type]string{},
		typeToKind:      map[reflect.Type][]string{},
		converter:       NewConverter(),
		InternalVersion: "",
		MetaFactory:     DefaultMetaFactory,
	}
	s.converter.NameFunc = s.nameFunc
	return s
}

// Log sets a logger on the scheme. For test purposes only
func (s *Scheme) Log(l DebugLogger) {
	s.converter.Debug = l
}

// nameFunc returns the name of the type that we wish to use for encoding. Defaults to
// the go name of the type if the type is not registered.
func (s *Scheme) nameFunc(t reflect.Type) string {
	if kind, ok := s.typeToKind[t]; ok {
		return kind[0]
	}
	return t.Name()
}

// AddKnownTypes registers all types passed in 'types' as being members of version 'version.
// Encode() will refuse objects unless their type has been registered with AddKnownTypes.
// All objects passed to types should be pointers to structs. The name that go reports for
// the struct becomes the "kind" field when encoding.
func (s *Scheme) AddKnownTypes(version string, types ...interface{}) {
	knownTypes, found := s.versionMap[version]
	if !found {
		knownTypes = map[string]reflect.Type{}
		s.versionMap[version] = knownTypes
	}
	for _, obj := range types {
		t := reflect.TypeOf(obj)
		if t.Kind() != reflect.Ptr {
			panic("All types must be pointers to structs.")
		}
		t = t.Elem()
		if t.Kind() != reflect.Struct {
			panic("All types must be pointers to structs.")
		}
		knownTypes[t.Name()] = t
		s.typeToVersion[t] = version
		s.typeToKind[t] = append(s.typeToKind[t], t.Name())
	}
}

// AddKnownTypeWithName is like AddKnownTypes, but it lets you specify what this type should
// be encoded as. Useful for testing when you don't want to make multiple packages to define
// your structs.
func (s *Scheme) AddKnownTypeWithName(version, kind string, obj interface{}) {
	knownTypes, found := s.versionMap[version]
	if !found {
		knownTypes = map[string]reflect.Type{}
		s.versionMap[version] = knownTypes
	}
	t := reflect.TypeOf(obj)
	if t.Kind() != reflect.Ptr {
		panic("All types must be pointers to structs.")
	}
	t = t.Elem()
	if t.Kind() != reflect.Struct {
		panic("All types must be pointers to structs.")
	}
	knownTypes[kind] = t
	s.typeToVersion[t] = version
	s.typeToKind[t] = append(s.typeToKind[t], kind)
}

// KnownTypes returns an array of the types that are known for a particular version.
func (s *Scheme) KnownTypes(version string) map[string]reflect.Type {
	all, ok := s.versionMap[version]
	if !ok {
		return map[string]reflect.Type{}
	}
	types := make(map[string]reflect.Type)
	for k, v := range all {
		types[k] = v
	}
	return types
}

// NewObject returns a new object of the given version and name,
// or an error if it hasn't been registered.
func (s *Scheme) NewObject(versionName, typeName string) (interface{}, error) {
	if types, ok := s.versionMap[versionName]; ok {
		if t, ok := types[typeName]; ok {
			return reflect.New(t).Interface(), nil
		}
		return nil, fmt.Errorf("No type '%v' for version '%v'", typeName, versionName)
	}
	return nil, fmt.Errorf("No version '%v'", versionName)
}

// AddConversionFuncs adds functions to the list of conversion functions. The given
// functions should know how to convert between two of your API objects, or their
// sub-objects. We deduce how to call these functions from the types of their two
// parameters; see the comment for Converter.Register.
//
// Note that, if you need to copy sub-objects that didn't change, you can use the
// conversion.Scope object that will be passed to your conversion function.
// Additionally, all conversions started by Scheme will set the SrcVersion and
// DestVersion fields on the Meta object. Example:
//
// s.AddConversionFuncs(
//	func(in *InternalObject, out *ExternalObject, scope conversion.Scope) error {
//		// You can depend on Meta() being non-nil, and this being set to
//		// the source version, e.g., ""
//		s.Meta().SrcVersion
//		// You can depend on this being set to the destination version,
//		// e.g., "v1beta1".
//		s.Meta().DestVersion
//		// Call scope.Convert to copy sub-fields.
//		s.Convert(&in.SubFieldThatMoved, &out.NewLocation.NewName, 0)
//		return nil
//	},
// )
//
// (For more detail about conversion functions, see Converter.Register's comment.)
//
// Also note that the default behavior, if you don't add a conversion function, is to
// sanely copy fields that have the same names and same type names. It's OK if the
// destination type has extra fields, but it must not remove any. So you only need to
// add conversion functions for things with changed/removed fields.
func (s *Scheme) AddConversionFuncs(conversionFuncs ...interface{}) error {
	for _, f := range conversionFuncs {
		err := s.converter.Register(f)
		if err != nil {
			return err
		}
	}
	return nil
}

// Convert will attempt to convert in into out. Both must be pointers. For easy
// testing of conversion functions. Returns an error if the conversion isn't
// possible. You can call this with types that haven't been registered (for example,
// a to test conversion of types that are nested within registered types), but in
// that case, the conversion.Scope object passed to your conversion functions won't
// have SrcVersion or DestVersion fields set correctly in Meta().
func (s *Scheme) Convert(in, out interface{}) error {
	inVersion := "unknown"
	outVersion := "unknown"
	if v, _, err := s.ObjectVersionAndKind(in); err == nil {
		inVersion = v
	}
	if v, _, err := s.ObjectVersionAndKind(out); err == nil {
		outVersion = v
	}
	return s.converter.Convert(in, out, 0, s.generateConvertMeta(inVersion, outVersion))
}

// generateConvertMeta constructs the meta value we pass to Convert.
func (s *Scheme) generateConvertMeta(srcVersion, destVersion string) *Meta {
	return &Meta{
		SrcVersion:  srcVersion,
		DestVersion: destVersion,
	}
}

// DataVersionAndKind will return the APIVersion and Kind of the given wire-format
// enconding of an API Object, or an error.
func (s *Scheme) DataVersionAndKind(data []byte) (version, kind string, err error) {
	return s.MetaFactory.Interpret(data)
}

// ObjectVersionAndKind returns the API version and kind of the go object,
// or an error if it's not a pointer or is unregistered.
func (s *Scheme) ObjectVersionAndKind(obj interface{}) (apiVersion, kind string, err error) {
	v, err := EnforcePtr(obj)
	if err != nil {
		return "", "", err
	}
	t := v.Type()
	version, vOK := s.typeToVersion[t]
	kinds, kOK := s.typeToKind[t]
	if !vOK || !kOK {
		return "", "", fmt.Errorf("Unregistered type: %v", t)
	}
	apiVersion = version
	kind = kinds[0]
	return
}

// SetVersionAndKind sets the version and kind fields (with help from
// MetaInsertionFactory). Returns an error if this isn't possible. obj
// must be a pointer.
func (s *Scheme) SetVersionAndKind(version, kind string, obj interface{}) error {
	return s.MetaFactory.Update(version, kind, obj)
}

// maybeCopy copies obj if it is not a pointer, to get a settable/addressable
// object. Guaranteed to return a pointer.
func maybeCopy(obj interface{}) interface{} {
	v := reflect.ValueOf(obj)
	if v.Kind() == reflect.Ptr {
		return obj
	}
	v2 := reflect.New(v.Type())
	v2.Elem().Set(v)
	return v2.Interface()
}
<commit_msg>Typo in conversion Godoc<commit_after>/*
Copyright 2014 Google Inc. All rights reserved.

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 conversion

import (
	"fmt"
	"reflect"
)

// Scheme defines an entire encoding and decoding scheme.
type Scheme struct {
	// versionMap allows one to figure out the go type of an object with
	// the given version and name.
	versionMap map[string]map[string]reflect.Type

	// typeToVersion allows one to figure out the version for a given go object.
	// The reflect.Type we index by should *not* be a pointer. If the same type
	// is registered for multiple versions, the last one wins.
	typeToVersion map[reflect.Type]string

	// typeToKind allows one to figure out the desired "kind" field for a given
	// go object. Requirements and caveats are the same as typeToVersion.
	typeToKind map[reflect.Type][]string

	// converter stores all registered conversion functions. It also has
	// default coverting behavior.
	converter *Converter

	// Indent will cause the JSON output from Encode to be indented, iff it is true.
	Indent bool

	// InternalVersion is the default internal version. It is recommended that
	// you use "" for the internal version.
	InternalVersion string

	// MetaInsertionFactory is used to create an object to store and retrieve
	// the version and kind information for all objects. The default uses the
	// keys "apiVersion" and "kind" respectively.
	MetaFactory MetaFactory
}

// NewScheme manufactures a new scheme.
func NewScheme() *Scheme {
	s := &Scheme{
		versionMap:      map[string]map[string]reflect.Type{},
		typeToVersion:   map[reflect.Type]string{},
		typeToKind:      map[reflect.Type][]string{},
		converter:       NewConverter(),
		InternalVersion: "",
		MetaFactory:     DefaultMetaFactory,
	}
	s.converter.NameFunc = s.nameFunc
	return s
}

// Log sets a logger on the scheme. For test purposes only
func (s *Scheme) Log(l DebugLogger) {
	s.converter.Debug = l
}

// nameFunc returns the name of the type that we wish to use for encoding. Defaults to
// the go name of the type if the type is not registered.
func (s *Scheme) nameFunc(t reflect.Type) string {
	if kind, ok := s.typeToKind[t]; ok {
		return kind[0]
	}
	return t.Name()
}

// AddKnownTypes registers all types passed in 'types' as being members of version 'version.
// Encode() will refuse objects unless their type has been registered with AddKnownTypes.
// All objects passed to types should be pointers to structs. The name that go reports for
// the struct becomes the "kind" field when encoding.
func (s *Scheme) AddKnownTypes(version string, types ...interface{}) {
	knownTypes, found := s.versionMap[version]
	if !found {
		knownTypes = map[string]reflect.Type{}
		s.versionMap[version] = knownTypes
	}
	for _, obj := range types {
		t := reflect.TypeOf(obj)
		if t.Kind() != reflect.Ptr {
			panic("All types must be pointers to structs.")
		}
		t = t.Elem()
		if t.Kind() != reflect.Struct {
			panic("All types must be pointers to structs.")
		}
		knownTypes[t.Name()] = t
		s.typeToVersion[t] = version
		s.typeToKind[t] = append(s.typeToKind[t], t.Name())
	}
}

// AddKnownTypeWithName is like AddKnownTypes, but it lets you specify what this type should
// be encoded as. Useful for testing when you don't want to make multiple packages to define
// your structs.
func (s *Scheme) AddKnownTypeWithName(version, kind string, obj interface{}) {
	knownTypes, found := s.versionMap[version]
	if !found {
		knownTypes = map[string]reflect.Type{}
		s.versionMap[version] = knownTypes
	}
	t := reflect.TypeOf(obj)
	if t.Kind() != reflect.Ptr {
		panic("All types must be pointers to structs.")
	}
	t = t.Elem()
	if t.Kind() != reflect.Struct {
		panic("All types must be pointers to structs.")
	}
	knownTypes[kind] = t
	s.typeToVersion[t] = version
	s.typeToKind[t] = append(s.typeToKind[t], kind)
}

// KnownTypes returns an array of the types that are known for a particular version.
func (s *Scheme) KnownTypes(version string) map[string]reflect.Type {
	all, ok := s.versionMap[version]
	if !ok {
		return map[string]reflect.Type{}
	}
	types := make(map[string]reflect.Type)
	for k, v := range all {
		types[k] = v
	}
	return types
}

// NewObject returns a new object of the given version and name,
// or an error if it hasn't been registered.
func (s *Scheme) NewObject(versionName, typeName string) (interface{}, error) {
	if types, ok := s.versionMap[versionName]; ok {
		if t, ok := types[typeName]; ok {
			return reflect.New(t).Interface(), nil
		}
		return nil, fmt.Errorf("No type '%v' for version '%v'", typeName, versionName)
	}
	return nil, fmt.Errorf("No version '%v'", versionName)
}

// AddConversionFuncs adds functions to the list of conversion functions. The given
// functions should know how to convert between two of your API objects, or their
// sub-objects. We deduce how to call these functions from the types of their two
// parameters; see the comment for Converter.Register.
//
// Note that, if you need to copy sub-objects that didn't change, you can use the
// conversion.Scope object that will be passed to your conversion function.
// Additionally, all conversions started by Scheme will set the SrcVersion and
// DestVersion fields on the Meta object. Example:
//
// s.AddConversionFuncs(
//	func(in *InternalObject, out *ExternalObject, scope conversion.Scope) error {
//		// You can depend on Meta() being non-nil, and this being set to
//		// the source version, e.g., ""
//		s.Meta().SrcVersion
//		// You can depend on this being set to the destination version,
//		// e.g., "v1beta1".
//		s.Meta().DestVersion
//		// Call scope.Convert to copy sub-fields.
//		s.Convert(&in.SubFieldThatMoved, &out.NewLocation.NewName, 0)
//		return nil
//	},
// )
//
// (For more detail about conversion functions, see Converter.Register's comment.)
//
// Also note that the default behavior, if you don't add a conversion function, is to
// sanely copy fields that have the same names and same type names. It's OK if the
// destination type has extra fields, but it must not remove any. So you only need to
// add conversion functions for things with changed/removed fields.
func (s *Scheme) AddConversionFuncs(conversionFuncs ...interface{}) error {
	for _, f := range conversionFuncs {
		err := s.converter.Register(f)
		if err != nil {
			return err
		}
	}
	return nil
}

// Convert will attempt to convert in into out. Both must be pointers. For easy
// testing of conversion functions. Returns an error if the conversion isn't
// possible. You can call this with types that haven't been registered (for example,
// a to test conversion of types that are nested within registered types), but in
// that case, the conversion.Scope object passed to your conversion functions won't
// have SrcVersion or DestVersion fields set correctly in Meta().
func (s *Scheme) Convert(in, out interface{}) error {
	inVersion := "unknown"
	outVersion := "unknown"
	if v, _, err := s.ObjectVersionAndKind(in); err == nil {
		inVersion = v
	}
	if v, _, err := s.ObjectVersionAndKind(out); err == nil {
		outVersion = v
	}
	return s.converter.Convert(in, out, 0, s.generateConvertMeta(inVersion, outVersion))
}

// generateConvertMeta constructs the meta value we pass to Convert.
func (s *Scheme) generateConvertMeta(srcVersion, destVersion string) *Meta {
	return &Meta{
		SrcVersion:  srcVersion,
		DestVersion: destVersion,
	}
}

// DataVersionAndKind will return the APIVersion and Kind of the given wire-format
// encoding of an API Object, or an error.
func (s *Scheme) DataVersionAndKind(data []byte) (version, kind string, err error) {
	return s.MetaFactory.Interpret(data)
}

// ObjectVersionAndKind returns the API version and kind of the go object,
// or an error if it's not a pointer or is unregistered.
func (s *Scheme) ObjectVersionAndKind(obj interface{}) (apiVersion, kind string, err error) {
	v, err := EnforcePtr(obj)
	if err != nil {
		return "", "", err
	}
	t := v.Type()
	version, vOK := s.typeToVersion[t]
	kinds, kOK := s.typeToKind[t]
	if !vOK || !kOK {
		return "", "", fmt.Errorf("Unregistered type: %v", t)
	}
	apiVersion = version
	kind = kinds[0]
	return
}

// SetVersionAndKind sets the version and kind fields (with help from
// MetaInsertionFactory). Returns an error if this isn't possible. obj
// must be a pointer.
func (s *Scheme) SetVersionAndKind(version, kind string, obj interface{}) error {
	return s.MetaFactory.Update(version, kind, obj)
}

// maybeCopy copies obj if it is not a pointer, to get a settable/addressable
// object. Guaranteed to return a pointer.
func maybeCopy(obj interface{}) interface{} {
	v := reflect.ValueOf(obj)
	if v.Kind() == reflect.Ptr {
		return obj
	}
	v2 := reflect.New(v.Type())
	v2.Elem().Set(v)
	return v2.Interface()
}
<|endoftext|>


<commit_before>package mocks

import "github.com/materials-commons/testify/mock"

import (
	"fmt"
	"github.com/materials-commons/mcstore/pkg/db/schema"
)

type Dirs struct {
	mock.Mock
}

func NewMDirs() *Dirs {
	return &Dirs{}
}

func (m *Dirs) ByID(id string) (*schema.Directory, error) {
	ret := m.Called(id)

	r0 := ret.Get(0).(*schema.Directory)
	r1 := ret.Error(1)

	return r0, r1
}

func (m *Dirs) ByPath(path, projectID string) (*schema.Directory, error) {
	ret := m.Called(path, projectID)
	r0 := ret.Get(0).(*schema.Directory)
	r1 := ret.Error(1)
	return r0, r1
}

func (m *Dirs) Files(dirID string) ([]schema.File, error) {
	ret := m.Called(dirID)
	r0 := ret.Get(0).([]schema.File)
	r1 := ret.Error(1)
	return r0, r1
}

func (m *Dirs) Insert(dir *schema.Directory) (*schema.Directory, error) {
	ret := m.Called(dir)
	r0 := ret.Get(0).(*schema.Directory)
	r1 := ret.Error(1)
	return r0, r1
}

type entry struct {
	dir   *schema.Directory
	err   error
	files []schema.File
}

type Dirs2 struct {
	method        map[string]*entry
	currentMethod string
}

func NewMDirs2() *Dirs2 {
	return &Dirs2{
		method: make(map[string]*entry),
	}
}

func (m *Dirs2) lookup(method string) *entry {
	if e, ok := m.method[method]; ok {
		return e
	}
	panic(fmt.Sprintf("Unable to find method: %s", method))
}

func (m *Dirs2) ByID(id string) (*schema.Directory, error) {
	e := m.lookup("ByID")
	return e.dir, e.err
}

func (m *Dirs2) ByPath(path, projectID string) (*schema.Directory, error) {
	e := m.lookup("ByPath")
	return e.dir, e.err
}

func (m *Dirs2) Files(dirID string) ([]schema.File, error) {
	e := m.lookup("Files")
	return e.files, e.err
}

func (m *Dirs2) Insert(dir *schema.Directory) (*schema.Directory, error) {
	e := m.lookup("Insert")
	return e.dir, e.err
}

func (m *Dirs2) On(method string) *Dirs2 {
	m.currentMethod = method
	m.method[method] = &entry{}
	return m
}

func (m *Dirs2) SetError(err error) *Dirs2 {
	m.method[m.currentMethod].err = err
	return m
}

func (m *Dirs2) SetDir(dir *schema.Directory) *Dirs2 {
	m.method[m.currentMethod].dir = dir
	return m
}

func (m *Dirs2) SetFiles(files []schema.File) *Dirs2 {
	m.method[m.currentMethod].files = files
	return m
}
<commit_msg>Rename entry to dentry.<commit_after>package mocks

import "github.com/materials-commons/testify/mock"

import (
	"fmt"
	"github.com/materials-commons/mcstore/pkg/db/schema"
)

type Dirs struct {
	mock.Mock
}

func NewMDirs() *Dirs {
	return &Dirs{}
}

func (m *Dirs) ByID(id string) (*schema.Directory, error) {
	ret := m.Called(id)

	r0 := ret.Get(0).(*schema.Directory)
	r1 := ret.Error(1)

	return r0, r1
}

func (m *Dirs) ByPath(path, projectID string) (*schema.Directory, error) {
	ret := m.Called(path, projectID)
	r0 := ret.Get(0).(*schema.Directory)
	r1 := ret.Error(1)
	return r0, r1
}

func (m *Dirs) Files(dirID string) ([]schema.File, error) {
	ret := m.Called(dirID)
	r0 := ret.Get(0).([]schema.File)
	r1 := ret.Error(1)
	return r0, r1
}

func (m *Dirs) Insert(dir *schema.Directory) (*schema.Directory, error) {
	ret := m.Called(dir)
	r0 := ret.Get(0).(*schema.Directory)
	r1 := ret.Error(1)
	return r0, r1
}

type dentry struct {
	dir   *schema.Directory
	err   error
	files []schema.File
}

type Dirs2 struct {
	method        map[string]*dentry
	currentMethod string
}

func NewMDirs2() *Dirs2 {
	return &Dirs2{
		method: make(map[string]*dentry),
	}
}

func (m *Dirs2) lookup(method string) *dentry {
	if e, ok := m.method[method]; ok {
		return e
	}
	panic(fmt.Sprintf("Unable to find method: %s", method))
}

func (m *Dirs2) ByID(id string) (*schema.Directory, error) {
	e := m.lookup("ByID")
	return e.dir, e.err
}

func (m *Dirs2) ByPath(path, projectID string) (*schema.Directory, error) {
	e := m.lookup("ByPath")
	return e.dir, e.err
}

func (m *Dirs2) Files(dirID string) ([]schema.File, error) {
	e := m.lookup("Files")
	return e.files, e.err
}

func (m *Dirs2) Insert(dir *schema.Directory) (*schema.Directory, error) {
	e := m.lookup("Insert")
	return e.dir, e.err
}

func (m *Dirs2) On(method string) *Dirs2 {
	m.currentMethod = method
	m.method[method] = &dentry{}
	return m
}

func (m *Dirs2) SetError(err error) *Dirs2 {
	m.method[m.currentMethod].err = err
	return m
}

func (m *Dirs2) SetDir(dir *schema.Directory) *Dirs2 {
	m.method[m.currentMethod].dir = dir
	return m
}

func (m *Dirs2) SetFiles(files []schema.File) *Dirs2 {
	m.method[m.currentMethod].files = files
	return m
}
<|endoftext|>


<commit_before>// Copyright 2019 The GCR Cleaner 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 gcrcleaner

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"time"
)

const (
	contentTypeHeader = "Content-Type"
	contentTypeJSON   = "application/json"
)

// Server is a cleaning server.
type Server struct {
	cleaner *Cleaner
}

// NewServer creates a new server for handler functions.
func NewServer(cleaner *Cleaner) (*Server, error) {
	if cleaner == nil {
		return nil, fmt.Errorf("missing cleaner")
	}

	return &Server{
		cleaner: cleaner,
	}, nil
}

// PubSubHandler is an http handler that invokes the cleaner from a pubsub
// request. Unlike an HTTP request, the pubsub endpoint always returns a success
// unless the pubsub message is malformed.
func (s *Server) PubSubHandler(cache Cache) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var m pubsubMessage
		if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
			err = fmt.Errorf("failed to decode pubsub message: %w", err)
			s.handleError(w, err, 400)
			return
		}

		// PubSub is "at least once" delivery. The cleaner is idempotent, but
		// let's try to prevent unnecessary work by not processing messages we've
		// already received.
		msgID := m.Subscription + "/" + m.Message.ID
		if exists := cache.Insert(msgID); exists {
			log.Printf("already processed message %s", msgID)
			w.WriteHeader(204)
			return
		}

		if len(m.Message.Data) == 0 {
			err := fmt.Errorf("missing data in pubsub payload")
			s.handleError(w, err, 400)
			return
		}

		// Start a goroutine to delete the images
		body := ioutil.NopCloser(bytes.NewReader(m.Message.Data))
		go func() {
			if _, _, err := s.clean(body); err != nil {
				log.Printf("error async: %s", err.Error())
			}
		}()

		w.WriteHeader(204)
	}
}

// HTTPHandler is an http handler that invokes the cleaner with the given
// parameters.
func (s *Server) HTTPHandler() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		deleted, status, err := s.clean(r.Body)
		if err != nil {
			s.handleError(w, err, status)
			return
		}

		b, err := json.Marshal(&cleanResp{
			Count: len(deleted),
			Refs:  deleted,
		})
		if err != nil {
			err = fmt.Errorf("failed to marshal JSON errors: %w", err)
			s.handleError(w, err, 500)
			return
		}

		w.WriteHeader(200)
		w.Header().Set(contentTypeHeader, contentTypeJSON)
		fmt.Fprint(w, string(b))
	}
}

// clean reads the given body as JSON and starts a cleaner instance.
func (s *Server) clean(r io.ReadCloser) ([]string, int, error) {
	var p Payload
	if err := json.NewDecoder(r).Decode(&p); err != nil {
		return nil, 500, fmt.Errorf("failed to decode payload as JSON: %w", err)
	}

	repo := p.Repo

	// Convert duration to a negative value, since we're about to "add" it to the
	// since time.
	sub := time.Duration(p.Grace)
	if p.Grace > 0 {
		sub = sub * -1
	}

	since := time.Now().UTC().Add(sub)
	allowTagged := p.AllowTagged
	keep := p.Keep

	log.Printf("deleting refs for %s since %s\n", repo, since)

	deleted, err := s.cleaner.Clean(repo, since, allowTagged, keep)
	if err != nil {
		return nil, 400, fmt.Errorf("failed to clean: %w", err)
	}

	log.Printf("deleted %d refs for %s", len(deleted), repo)

	return deleted, 200, nil
}

// handleError returns a JSON-formatted error message
func (s *Server) handleError(w http.ResponseWriter, err error, status int) {
	log.Printf("error %d: %s", status, err.Error())

	b, err := json.Marshal(&errorResp{Error: err.Error()})
	if err != nil {
		err = fmt.Errorf("failed to marshal JSON errors: %w", err)
		http.Error(w, err.Error(), 500)
		return
	}

	w.WriteHeader(status)
	w.Header().Set(contentTypeHeader, contentTypeJSON)
	fmt.Fprint(w, string(b))
}

// Payload is the expected incoming payload format.
type Payload struct {
	// Repo is the name of the repo in the format gcr.io/foo/bar
	Repo string `json:"repo"`

	// Grace is a time.Duration value indicating how much grade period should be
	// given to new, untagged layers. The default is no grace.
	Grace duration `json:"grace"`

	// AllowTagged is a Boolean value determine if tagged images are allowed
	// to be deleted.
	AllowTagged bool `json:"allow_tagged"`

	// Keep is an the minimum number of images to keep.
	Keep int `json:"keep"`
}

type pubsubMessage struct {
	Message struct {
		Data []byte `json:"data"`
		ID   string `json:"message_id"`
	} `json:"message"`
	Subscription string `json:"subscription"`
}

type cleanResp struct {
	Count int      `json:"count"`
	Refs  []string `json:"refs"`
}

type errorResp struct {
	Error string `json:"error"`
}

type duration time.Duration

func (d duration) MarshalJSON() ([]byte, error) {
	return json.Marshal(time.Duration(d).String())
}

func (d *duration) UnmarshalJSON(b []byte) error {
	var v interface{}
	if err := json.Unmarshal(b, &v); err != nil {
		return err
	}

	switch val := v.(type) {
	case float64:
		*d = duration(time.Duration(val))
		return nil
	case string:
		s, err := time.ParseDuration(val)
		if err != nil {
			return nil
		}
		*d = duration(s)
		return nil
	default:
		return fmt.Errorf("invalid duration type %T", val)
	}
}
<commit_msg>Grammar<commit_after>// Copyright 2019 The GCR Cleaner 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 gcrcleaner

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"time"
)

const (
	contentTypeHeader = "Content-Type"
	contentTypeJSON   = "application/json"
)

// Server is a cleaning server.
type Server struct {
	cleaner *Cleaner
}

// NewServer creates a new server for handler functions.
func NewServer(cleaner *Cleaner) (*Server, error) {
	if cleaner == nil {
		return nil, fmt.Errorf("missing cleaner")
	}

	return &Server{
		cleaner: cleaner,
	}, nil
}

// PubSubHandler is an http handler that invokes the cleaner from a pubsub
// request. Unlike an HTTP request, the pubsub endpoint always returns a success
// unless the pubsub message is malformed.
func (s *Server) PubSubHandler(cache Cache) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var m pubsubMessage
		if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
			err = fmt.Errorf("failed to decode pubsub message: %w", err)
			s.handleError(w, err, 400)
			return
		}

		// PubSub is "at least once" delivery. The cleaner is idempotent, but
		// let's try to prevent unnecessary work by not processing messages we've
		// already received.
		msgID := m.Subscription + "/" + m.Message.ID
		if exists := cache.Insert(msgID); exists {
			log.Printf("already processed message %s", msgID)
			w.WriteHeader(204)
			return
		}

		if len(m.Message.Data) == 0 {
			err := fmt.Errorf("missing data in pubsub payload")
			s.handleError(w, err, 400)
			return
		}

		// Start a goroutine to delete the images
		body := ioutil.NopCloser(bytes.NewReader(m.Message.Data))
		go func() {
			if _, _, err := s.clean(body); err != nil {
				log.Printf("error async: %s", err.Error())
			}
		}()

		w.WriteHeader(204)
	}
}

// HTTPHandler is an http handler that invokes the cleaner with the given
// parameters.
func (s *Server) HTTPHandler() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		deleted, status, err := s.clean(r.Body)
		if err != nil {
			s.handleError(w, err, status)
			return
		}

		b, err := json.Marshal(&cleanResp{
			Count: len(deleted),
			Refs:  deleted,
		})
		if err != nil {
			err = fmt.Errorf("failed to marshal JSON errors: %w", err)
			s.handleError(w, err, 500)
			return
		}

		w.WriteHeader(200)
		w.Header().Set(contentTypeHeader, contentTypeJSON)
		fmt.Fprint(w, string(b))
	}
}

// clean reads the given body as JSON and starts a cleaner instance.
func (s *Server) clean(r io.ReadCloser) ([]string, int, error) {
	var p Payload
	if err := json.NewDecoder(r).Decode(&p); err != nil {
		return nil, 500, fmt.Errorf("failed to decode payload as JSON: %w", err)
	}

	repo := p.Repo

	// Convert duration to a negative value, since we're about to "add" it to the
	// since time.
	sub := time.Duration(p.Grace)
	if p.Grace > 0 {
		sub = sub * -1
	}

	since := time.Now().UTC().Add(sub)
	allowTagged := p.AllowTagged
	keep := p.Keep

	log.Printf("deleting refs for %s since %s\n", repo, since)

	deleted, err := s.cleaner.Clean(repo, since, allowTagged, keep)
	if err != nil {
		return nil, 400, fmt.Errorf("failed to clean: %w", err)
	}

	log.Printf("deleted %d refs for %s", len(deleted), repo)

	return deleted, 200, nil
}

// handleError returns a JSON-formatted error message
func (s *Server) handleError(w http.ResponseWriter, err error, status int) {
	log.Printf("error %d: %s", status, err.Error())

	b, err := json.Marshal(&errorResp{Error: err.Error()})
	if err != nil {
		err = fmt.Errorf("failed to marshal JSON errors: %w", err)
		http.Error(w, err.Error(), 500)
		return
	}

	w.WriteHeader(status)
	w.Header().Set(contentTypeHeader, contentTypeJSON)
	fmt.Fprint(w, string(b))
}

// Payload is the expected incoming payload format.
type Payload struct {
	// Repo is the name of the repo in the format gcr.io/foo/bar
	Repo string `json:"repo"`

	// Grace is a time.Duration value indicating how much grade period should be
	// given to new, untagged layers. The default is no grace.
	Grace duration `json:"grace"`

	// AllowTagged is a Boolean value determine if tagged images are allowed
	// to be deleted.
	AllowTagged bool `json:"allow_tagged"`

	// Keep is the minimum number of images to keep.
	Keep int `json:"keep"`
}

type pubsubMessage struct {
	Message struct {
		Data []byte `json:"data"`
		ID   string `json:"message_id"`
	} `json:"message"`
	Subscription string `json:"subscription"`
}

type cleanResp struct {
	Count int      `json:"count"`
	Refs  []string `json:"refs"`
}

type errorResp struct {
	Error string `json:"error"`
}

type duration time.Duration

func (d duration) MarshalJSON() ([]byte, error) {
	return json.Marshal(time.Duration(d).String())
}

func (d *duration) UnmarshalJSON(b []byte) error {
	var v interface{}
	if err := json.Unmarshal(b, &v); err != nil {
		return err
	}

	switch val := v.(type) {
	case float64:
		*d = duration(time.Duration(val))
		return nil
	case string:
		s, err := time.ParseDuration(val)
		if err != nil {
			return nil
		}
		*d = duration(s)
		return nil
	default:
		return fmt.Errorf("invalid duration type %T", val)
	}
}
<|endoftext|>


<commit_before>/*
Copyright 2019 The HAProxy Ingress Controller 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 haproxy

import (
	"fmt"
	"reflect"
	"sort"
	"strconv"

	hatypes "github.com/jcmoraisjr/haproxy-ingress/pkg/haproxy/types"
	"github.com/jcmoraisjr/haproxy-ingress/pkg/haproxy/utils"
	"github.com/jcmoraisjr/haproxy-ingress/pkg/types"
)

type dynUpdater struct {
	logger types.Logger
	old    *config
	cur    *config
	socket string
	cmd    func(socket string, commands ...string) ([]string, error)
	cmdCnt int
}

type backendPair struct {
	old *hatypes.Backend
	cur *hatypes.Backend
}

type epPair struct {
	old *hatypes.Endpoint
	cur *hatypes.Endpoint
}

func (i *instance) newDynUpdater() *dynUpdater {
	var old, cur *config
	if i.oldConfig != nil {
		old = i.oldConfig.(*config)
	}
	if i.curConfig != nil {
		cur = i.curConfig.(*config)
	}
	return &dynUpdater{
		logger: i.logger,
		old:    old,
		cur:    cur,
		socket: i.curConfig.Global().AdminSocket,
		cmd:    utils.HAProxyCommand,
	}
}

func (d *dynUpdater) update() bool {
	updated := d.checkConfigPair()
	if !updated {
		// Need to reload, time to adjust empty slots according to config
		d.alignSlots()
	}
	return updated
}

func (d *dynUpdater) checkConfigPair() bool {
	oldConfig := d.old
	curConfig := d.cur
	if oldConfig == nil || curConfig == nil {
		return false
	}

	// check equality of everything but backends
	oldConfigCopy := *oldConfig
	oldConfigCopy.backends = curConfig.backends
	oldConfigCopy.defaultBackend = curConfig.defaultBackend
	if !reflect.DeepEqual(&oldConfigCopy, curConfig) {
		var diff []string
		if !reflect.DeepEqual(oldConfig.global, curConfig.global) {
			diff = append(diff, "global")
		}
		if !reflect.DeepEqual(oldConfig.tcpbackends, curConfig.tcpbackends) {
			diff = append(diff, "tcp-services")
		}
		if !reflect.DeepEqual(oldConfig.hosts, curConfig.hosts) {
			diff = append(diff, "hosts")
		}
		if !reflect.DeepEqual(oldConfig.userlists, curConfig.userlists) {
			diff = append(diff, "userlists")
		}
		d.logger.InfoV(2, "diff outside backends - %v", diff)
		return false
	}

	// map backends of old and new config together
	// return false if len or names doesn't match
	if len(curConfig.backends) != len(curConfig.backends) {
		d.logger.InfoV(2, "added or removed backend(s)")
		return false
	}
	backends := make(map[string]*backendPair, len(oldConfig.backends))
	for _, backend := range oldConfig.backends {
		backends[backend.ID] = &backendPair{old: backend}
	}
	for _, backend := range curConfig.backends {
		back, found := backends[backend.ID]
		if !found {
			d.logger.InfoV(2, "removed backend '%s'", backend.ID)
			return false
		}
		back.cur = backend
	}

	// try to dynamically update every single backend
	// true if deep equals or sucessfully updated
	// false if cannot be dynamically updated or update failed
	for _, pair := range backends {
		if !d.checkBackendPair(pair) {
			return false
		}
	}

	return true
}

func (d *dynUpdater) checkBackendPair(pair *backendPair) bool {
	oldBack := pair.old
	curBack := pair.cur

	// check equality of everything but endpoints
	oldBackCopy := *oldBack
	oldBackCopy.Dynamic = curBack.Dynamic
	oldBackCopy.Endpoints = curBack.Endpoints
	if !reflect.DeepEqual(&oldBackCopy, curBack) {
		d.logger.InfoV(2, "diff outside endpoints of backend '%s'", curBack.ID)
		return false
	}

	// can decrease endpoints, cannot increase
	if len(oldBack.Endpoints) < len(curBack.Endpoints) {
		d.logger.InfoV(2, "added endpoints on backend '%s'", curBack.ID)
		return false
	}

	// Resolver == update via DNS discovery
	if curBack.Resolver != "" {
		return true
	}

	// most of the backends are equal, save some proc stopping here if deep equals
	if reflect.DeepEqual(oldBack.Endpoints, curBack.Endpoints) {
		return true
	}

	// oldBack and curBack differs, DynUpdate is disabled, need to reload
	// TODO check if endpoints are the same and only the order differ
	if !curBack.Dynamic.DynUpdate {
		d.logger.InfoV(2, "backend '%s' changed and its dynamic-scaling is 'false'", curBack.ID)
		return false
	}

	// map endpoints of old and new config together
	endpoints := make(map[string]*epPair, len(oldBack.Endpoints))
	targets := make([]string, 0, len(oldBack.Endpoints))
	var empty []string
	for _, endpoint := range oldBack.Endpoints {
		if endpoint.Enabled {
			endpoints[endpoint.Target] = &epPair{old: endpoint}
			targets = append(targets, endpoint.Target)
		} else {
			empty = append(empty, endpoint.Name)
		}
	}

	// From this point we cannot simply `return false` because endpoint.Name
	// is being updated, need to be updated until the end, and endpoints slice
	// need to be sorted
	updated := true

	// reuse the backend/server which has the same target endpoint, if found,
	// this will save some socket calls and will not mess endpoint metrics
	var added []*hatypes.Endpoint
	for _, endpoint := range curBack.Endpoints {
		if pair, found := endpoints[endpoint.Target]; found {
			endpoint.Name = pair.old.Name
			pair.cur = endpoint
		} else {
			added = append(added, endpoint)
		}
	}

	// try to dynamically remove/update/add endpoints
	// targets used here only to have predictable results
	// Endpoint.Label != "" means use-server of blue/green config, need reload
	sort.Strings(targets)
	for _, target := range targets {
		pair := endpoints[target]
		if pair.cur == nil {
			if pair.old.Label != "" || (updated && !d.execDisableEndpoint(curBack.ID, pair.old)) {
				updated = false
			}
			empty = append(empty, pair.old.Name)
		} else if updated && !d.checkEndpointPair(curBack.ID, pair) {
			updated = false
		}
	}
	for i := range added {
		// reusing empty slots from oldBack
		added[i].Name = empty[i]
		if added[i].Label != "" || (updated && !d.execEnableEndpoint(curBack.ID, nil, added[i])) {
			updated = false
		}
	}

	// copy remaining empty slots from oldBack to curBack, so it can be used in a future update
	for i := len(added); i < len(empty); i++ {
		curBack.AddEmptyEndpoint().Name = empty[i]
	}
	curBack.SortEndpoints()

	return updated
}

func (d *dynUpdater) checkEndpointPair(backname string, pair *epPair) bool {
	if reflect.DeepEqual(pair.old, pair.cur) {
		return true
	}
	if pair.old.Label != "" || pair.cur.Label != "" {
		return false
	}
	return d.execEnableEndpoint(backname, pair.old, pair.cur)
}

func (d *dynUpdater) alignSlots() {
	if d.cur == nil {
		return
	}
	for _, back := range d.cur.backends {
		if !back.Dynamic.DynUpdate {
			// no need to add empty slots if won't dynamically update
			continue
		}
		minFreeSlots := back.Dynamic.MinFreeSlots
		blockSize := back.Dynamic.BlockSize
		if blockSize < 1 {
			blockSize = 1
		}
		var newFreeSlots int
		if minFreeSlots == 0 && len(back.Endpoints) == 0 {
			newFreeSlots = blockSize
		} else {
			totalFreeSlots := 0
			for _, ep := range back.Endpoints {
				if ep.IsEmpty() {
					totalFreeSlots++
				}
			}
			for i := totalFreeSlots; i < minFreeSlots; i++ {
				back.AddEmptyEndpoint()
			}
			// * []endpoints == group of blocks
			// * block == group of slots
			// * slot == a single server
			// newFreeSlots := blockSize - (1 <= <size-of-last-block> <= blockSize)
			newFreeSlots = blockSize - (((len(back.Endpoints) + blockSize - 1) % blockSize) + 1)
		}
		for i := 0; i < newFreeSlots; i++ {
			back.AddEmptyEndpoint()
		}
	}
}

func (d *dynUpdater) execDisableEndpoint(backname string, ep *hatypes.Endpoint) bool {
	server := fmt.Sprintf("set server %s/%s ", backname, ep.Name)
	cmd := []string{
		server + "state maint",
		server + "addr 127.0.0.1 port 1023",
		server + "weight 0",
	}
	msg, err := d.execCommand(cmd)
	if err != nil {
		d.logger.Error("error disabling endpoint %s/%s: %v", backname, ep.Name, err)
		return false
	}
	d.logger.InfoV(2, "disabled endpoint '%s' on backend/server '%s/%s'", ep.Target, backname, ep.Name)
	for _, m := range msg {
		d.logger.InfoV(2, m)
	}
	return true
}

func (d *dynUpdater) execEnableEndpoint(backname string, oldEP, curEP *hatypes.Endpoint) bool {
	state := map[bool]string{true: "ready", false: "drain"}[curEP.Weight > 0]
	server := fmt.Sprintf("set server %s/%s ", backname, curEP.Name)
	cmd := []string{
		server + "addr " + curEP.IP + " port " + strconv.Itoa(curEP.Port),
		server + "state " + state,
		server + "weight " + strconv.Itoa(curEP.Weight),
	}
	msg, err := d.execCommand(cmd)
	if err != nil {
		d.logger.Error("error adding/updating endpoint %s/%s: %v", backname, curEP.Name, err)
		return false
	}
	event := map[bool]string{true: "updated", false: "added"}[oldEP != nil]
	d.logger.InfoV(2, "%s endpoint '%s' weight '%d' state '%s' on backend/server '%s/%s'",
		event, curEP.Target, curEP.Weight, state, backname, curEP.Name)
	for _, m := range msg {
		d.logger.InfoV(2, m)
	}
	return true
}

func (d *dynUpdater) execCommand(cmd []string) ([]string, error) {
	msg, err := d.cmd(d.socket, cmd...)
	d.cmdCnt = d.cmdCnt + len(cmd)
	return msg, err
}
<commit_msg>fixes dynamic update if acme config changes<commit_after>/*
Copyright 2019 The HAProxy Ingress Controller 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 haproxy

import (
	"fmt"
	"reflect"
	"sort"
	"strconv"

	hatypes "github.com/jcmoraisjr/haproxy-ingress/pkg/haproxy/types"
	"github.com/jcmoraisjr/haproxy-ingress/pkg/haproxy/utils"
	"github.com/jcmoraisjr/haproxy-ingress/pkg/types"
)

type dynUpdater struct {
	logger types.Logger
	old    *config
	cur    *config
	socket string
	cmd    func(socket string, commands ...string) ([]string, error)
	cmdCnt int
}

type backendPair struct {
	old *hatypes.Backend
	cur *hatypes.Backend
}

type epPair struct {
	old *hatypes.Endpoint
	cur *hatypes.Endpoint
}

func (i *instance) newDynUpdater() *dynUpdater {
	var old, cur *config
	if i.oldConfig != nil {
		old = i.oldConfig.(*config)
	}
	if i.curConfig != nil {
		cur = i.curConfig.(*config)
	}
	return &dynUpdater{
		logger: i.logger,
		old:    old,
		cur:    cur,
		socket: i.curConfig.Global().AdminSocket,
		cmd:    utils.HAProxyCommand,
	}
}

func (d *dynUpdater) update() bool {
	updated := d.checkConfigPair()
	if !updated {
		// Need to reload, time to adjust empty slots according to config
		d.alignSlots()
	}
	return updated
}

func (d *dynUpdater) checkConfigPair() bool {
	oldConfig := d.old
	curConfig := d.cur
	if oldConfig == nil || curConfig == nil {
		return false
	}

	// check equality of everything but backends
	oldConfigCopy := *oldConfig
	oldConfigCopy.backends = curConfig.backends
	oldConfigCopy.defaultBackend = curConfig.defaultBackend
	if !oldConfigCopy.Equals(curConfig) {
		var diff []string
		if !reflect.DeepEqual(oldConfig.global, curConfig.global) {
			diff = append(diff, "global")
		}
		if !reflect.DeepEqual(oldConfig.tcpbackends, curConfig.tcpbackends) {
			diff = append(diff, "tcp-services")
		}
		if !reflect.DeepEqual(oldConfig.hosts, curConfig.hosts) {
			diff = append(diff, "hosts")
		}
		if !reflect.DeepEqual(oldConfig.userlists, curConfig.userlists) {
			diff = append(diff, "userlists")
		}
		d.logger.InfoV(2, "diff outside backends - %v", diff)
		return false
	}

	// map backends of old and new config together
	// return false if len or names doesn't match
	if len(curConfig.backends) != len(curConfig.backends) {
		d.logger.InfoV(2, "added or removed backend(s)")
		return false
	}
	backends := make(map[string]*backendPair, len(oldConfig.backends))
	for _, backend := range oldConfig.backends {
		backends[backend.ID] = &backendPair{old: backend}
	}
	for _, backend := range curConfig.backends {
		back, found := backends[backend.ID]
		if !found {
			d.logger.InfoV(2, "removed backend '%s'", backend.ID)
			return false
		}
		back.cur = backend
	}

	// try to dynamically update every single backend
	// true if deep equals or sucessfully updated
	// false if cannot be dynamically updated or update failed
	for _, pair := range backends {
		if !d.checkBackendPair(pair) {
			return false
		}
	}

	return true
}

func (d *dynUpdater) checkBackendPair(pair *backendPair) bool {
	oldBack := pair.old
	curBack := pair.cur

	// check equality of everything but endpoints
	oldBackCopy := *oldBack
	oldBackCopy.Dynamic = curBack.Dynamic
	oldBackCopy.Endpoints = curBack.Endpoints
	if !reflect.DeepEqual(&oldBackCopy, curBack) {
		d.logger.InfoV(2, "diff outside endpoints of backend '%s'", curBack.ID)
		return false
	}

	// can decrease endpoints, cannot increase
	if len(oldBack.Endpoints) < len(curBack.Endpoints) {
		d.logger.InfoV(2, "added endpoints on backend '%s'", curBack.ID)
		return false
	}

	// Resolver == update via DNS discovery
	if curBack.Resolver != "" {
		return true
	}

	// most of the backends are equal, save some proc stopping here if deep equals
	if reflect.DeepEqual(oldBack.Endpoints, curBack.Endpoints) {
		return true
	}

	// oldBack and curBack differs, DynUpdate is disabled, need to reload
	// TODO check if endpoints are the same and only the order differ
	if !curBack.Dynamic.DynUpdate {
		d.logger.InfoV(2, "backend '%s' changed and its dynamic-scaling is 'false'", curBack.ID)
		return false
	}

	// map endpoints of old and new config together
	endpoints := make(map[string]*epPair, len(oldBack.Endpoints))
	targets := make([]string, 0, len(oldBack.Endpoints))
	var empty []string
	for _, endpoint := range oldBack.Endpoints {
		if endpoint.Enabled {
			endpoints[endpoint.Target] = &epPair{old: endpoint}
			targets = append(targets, endpoint.Target)
		} else {
			empty = append(empty, endpoint.Name)
		}
	}

	// From this point we cannot simply `return false` because endpoint.Name
	// is being updated, need to be updated until the end, and endpoints slice
	// need to be sorted
	updated := true

	// reuse the backend/server which has the same target endpoint, if found,
	// this will save some socket calls and will not mess endpoint metrics
	var added []*hatypes.Endpoint
	for _, endpoint := range curBack.Endpoints {
		if pair, found := endpoints[endpoint.Target]; found {
			endpoint.Name = pair.old.Name
			pair.cur = endpoint
		} else {
			added = append(added, endpoint)
		}
	}

	// try to dynamically remove/update/add endpoints
	// targets used here only to have predictable results
	// Endpoint.Label != "" means use-server of blue/green config, need reload
	sort.Strings(targets)
	for _, target := range targets {
		pair := endpoints[target]
		if pair.cur == nil {
			if pair.old.Label != "" || (updated && !d.execDisableEndpoint(curBack.ID, pair.old)) {
				updated = false
			}
			empty = append(empty, pair.old.Name)
		} else if updated && !d.checkEndpointPair(curBack.ID, pair) {
			updated = false
		}
	}
	for i := range added {
		// reusing empty slots from oldBack
		added[i].Name = empty[i]
		if added[i].Label != "" || (updated && !d.execEnableEndpoint(curBack.ID, nil, added[i])) {
			updated = false
		}
	}

	// copy remaining empty slots from oldBack to curBack, so it can be used in a future update
	for i := len(added); i < len(empty); i++ {
		curBack.AddEmptyEndpoint().Name = empty[i]
	}
	curBack.SortEndpoints()

	return updated
}

func (d *dynUpdater) checkEndpointPair(backname string, pair *epPair) bool {
	if reflect.DeepEqual(pair.old, pair.cur) {
		return true
	}
	if pair.old.Label != "" || pair.cur.Label != "" {
		return false
	}
	return d.execEnableEndpoint(backname, pair.old, pair.cur)
}

func (d *dynUpdater) alignSlots() {
	if d.cur == nil {
		return
	}
	for _, back := range d.cur.backends {
		if !back.Dynamic.DynUpdate {
			// no need to add empty slots if won't dynamically update
			continue
		}
		minFreeSlots := back.Dynamic.MinFreeSlots
		blockSize := back.Dynamic.BlockSize
		if blockSize < 1 {
			blockSize = 1
		}
		var newFreeSlots int
		if minFreeSlots == 0 && len(back.Endpoints) == 0 {
			newFreeSlots = blockSize
		} else {
			totalFreeSlots := 0
			for _, ep := range back.Endpoints {
				if ep.IsEmpty() {
					totalFreeSlots++
				}
			}
			for i := totalFreeSlots; i < minFreeSlots; i++ {
				back.AddEmptyEndpoint()
			}
			// * []endpoints == group of blocks
			// * block == group of slots
			// * slot == a single server
			// newFreeSlots := blockSize - (1 <= <size-of-last-block> <= blockSize)
			newFreeSlots = blockSize - (((len(back.Endpoints) + blockSize - 1) % blockSize) + 1)
		}
		for i := 0; i < newFreeSlots; i++ {
			back.AddEmptyEndpoint()
		}
	}
}

func (d *dynUpdater) execDisableEndpoint(backname string, ep *hatypes.Endpoint) bool {
	server := fmt.Sprintf("set server %s/%s ", backname, ep.Name)
	cmd := []string{
		server + "state maint",
		server + "addr 127.0.0.1 port 1023",
		server + "weight 0",
	}
	msg, err := d.execCommand(cmd)
	if err != nil {
		d.logger.Error("error disabling endpoint %s/%s: %v", backname, ep.Name, err)
		return false
	}
	d.logger.InfoV(2, "disabled endpoint '%s' on backend/server '%s/%s'", ep.Target, backname, ep.Name)
	for _, m := range msg {
		d.logger.InfoV(2, m)
	}
	return true
}

func (d *dynUpdater) execEnableEndpoint(backname string, oldEP, curEP *hatypes.Endpoint) bool {
	state := map[bool]string{true: "ready", false: "drain"}[curEP.Weight > 0]
	server := fmt.Sprintf("set server %s/%s ", backname, curEP.Name)
	cmd := []string{
		server + "addr " + curEP.IP + " port " + strconv.Itoa(curEP.Port),
		server + "state " + state,
		server + "weight " + strconv.Itoa(curEP.Weight),
	}
	msg, err := d.execCommand(cmd)
	if err != nil {
		d.logger.Error("error adding/updating endpoint %s/%s: %v", backname, curEP.Name, err)
		return false
	}
	event := map[bool]string{true: "updated", false: "added"}[oldEP != nil]
	d.logger.InfoV(2, "%s endpoint '%s' weight '%d' state '%s' on backend/server '%s/%s'",
		event, curEP.Target, curEP.Weight, state, backname, curEP.Name)
	for _, m := range msg {
		d.logger.InfoV(2, m)
	}
	return true
}

func (d *dynUpdater) execCommand(cmd []string) ([]string, error) {
	msg, err := d.cmd(d.socket, cmd...)
	d.cmdCnt = d.cmdCnt + len(cmd)
	return msg, err
}
<|endoftext|>


<commit_before>package install

import (
	"context"
	"errors"
	"fmt"
	"strconv"
	"strings"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/util/intstr"
	"k8s.io/apimachinery/pkg/util/validation"

	"github.com/datawire/ambassador/pkg/kates"
)

// svcPortByNameOrNumber iterates through a list of ports in a service and
// only returns the ports that match the given nameOrNumber
func svcPortByNameOrNumber(svc *kates.Service, nameOrNumber string) []*kates.ServicePort {
	svcPorts := make([]*kates.ServicePort, 0)
	ports := svc.Spec.Ports
	var isName bool
	validName := validation.IsValidPortName(nameOrNumber)
	if len(validName) > 0 {
		isName = false
	} else {
		isName = true
	}
	for i := range ports {
		port := &ports[i]
		matchFound := false
		// If no nameOrNumber has been specified, we include it
		if nameOrNumber == "" {
			matchFound = true
		}
		// If the nameOrNumber is a valid name, we compare it to the
		// name listed in the servicePort
		if isName {
			if nameOrNumber == port.Name {
				matchFound = true
			}
		} else {
			// Otherwise we compare it to the port number
			givenPort, err := strconv.Atoi(nameOrNumber)
			if err == nil && int32(givenPort) == port.Port {
				matchFound = true
			}
		}
		if matchFound {
			svcPorts = append(svcPorts, port)
		}
	}
	return svcPorts
}

func FindMatchingServices(c context.Context, client *kates.Client, portNameOrNumber, svcName, namespace string, labels map[string]string) ([]*kates.Service, error) {
	// TODO: Expensive on large clusters but the problem goes away once we move the installer to the traffic-manager
	var svcs []*kates.Service
	if err := client.List(c, kates.Query{Name: svcName, Kind: "Service", Namespace: namespace}, &svcs); err != nil {
		return nil, err
	}

	// Returns true if selector is completely included in labels
	labelsMatch := func(selector map[string]string) bool {
		if len(selector) == 0 || len(labels) < len(selector) {
			return false
		}
		for k, v := range selector {
			if labels[k] != v {
				return false
			}
		}
		return true
	}

	var matching []*kates.Service
	for _, svc := range svcs {
		if (svcName == "" || svc.Name == svcName) && labelsMatch(svc.Spec.Selector) && len(svcPortByNameOrNumber(svc, portNameOrNumber)) > 0 {
			matching = append(matching, svc)
		}
	}
	return matching, nil
}

func FindMatchingService(c context.Context, client *kates.Client, portNameOrNumber, svcName, namespace string, labels map[string]string) (*kates.Service, error) {
	matchingSvcs, err := FindMatchingServices(c, client, portNameOrNumber, svcName, namespace, labels)
	if err != nil {
		return nil, err
	}
	if len(matchingSvcs) == 1 {
		return matchingSvcs[0], nil
	}

	count := "no"
	suffix := ""
	portRef := ""
	if len(matchingSvcs) > 0 {
		svcNames := make([]string, len(matchingSvcs))
		for i, svc := range matchingSvcs {
			svcNames[i] = svc.Name
		}
		count = "multiple"
		suffix = fmt.Sprintf(", use --service and one of: %s", strings.Join(svcNames, ","))
	}
	if portNameOrNumber != "" {
		portRef = fmt.Sprintf(" and a port referenced by name or port number %s", portNameOrNumber)
	}
	return nil, fmt.Errorf("found %s services with a selector matching labels %v%s in namespace %s%s", count, labels, portRef, namespace, suffix)
}

// FindMatchingPort finds the matching container associated with portNameOrNumber
// in the given service.
func FindMatchingPort(cns []corev1.Container, portNameOrNumber string, svc *kates.Service) (
	sPort *kates.ServicePort,
	cn *kates.Container,
	cPortIndex int,
	err error,
) {
	// For now, we only support intercepting one port on a given service.
	ports := svcPortByNameOrNumber(svc, portNameOrNumber)
	switch numPorts := len(ports); {
	case numPorts == 0:
		// this may happen when portNameOrNumber is specified but none of the
		// ports match
		return nil, nil, 0, errors.New("found no Service with a port that matches any container in this workload")

	case numPorts > 1:
		return nil, nil, 0, errors.New(`found matching Service with multiple matching ports.
Please specify the Service port you want to intercept by passing the --port=local:svcPortName flag.`)
	default:
	}
	port := ports[0]
	var matchingServicePort *corev1.ServicePort
	var matchingContainer *corev1.Container
	var containerPortIndex int

	if port.TargetPort.Type == intstr.String {
		portName := port.TargetPort.StrVal
		for ci := 0; ci < len(cns) && matchingContainer == nil; ci++ {
			cn := &cns[ci]
			for pi := range cn.Ports {
				if cn.Ports[pi].Name == portName {
					matchingServicePort = port
					matchingContainer = cn
					containerPortIndex = pi
					break
				}
			}
		}
	} else {
		portNum := port.TargetPort.IntVal
		// First see if we have a container with a matching port
	containerLoop:
		for ci := range cns {
			cn := &cns[ci]
			for pi := range cn.Ports {
				if cn.Ports[pi].ContainerPort == portNum {
					matchingServicePort = port
					matchingContainer = cn
					containerPortIndex = pi
					break
				}
			}
		}
		// If no container matched, then use the first container with no ports at all. This
		// enables intercepts of containers that indeed do listen a port but lack a matching
		// port description in the manifest, which is what you get if you do:
		//     kubectl create deploy my-deploy --image my-image
		//     kubectl expose deploy my-deploy --port 80 --target-port 8080
		if matchingContainer == nil {
			for ci := 0; ci < len(cns); ci++ {
				cn := &cns[ci]
				if len(cn.Ports) == 0 {
					matchingServicePort = port
					matchingContainer = cn
					containerPortIndex = -1
					break
				}
			}
		}
	}

	if matchingServicePort == nil {
		return nil, nil, 0, errors.New("found no Service with a port that matches any container in this workload")
	}
	return matchingServicePort, matchingContainer, containerPortIndex, nil
}
<commit_msg>Update pkg/install/discovery.go<commit_after>package install

import (
	"context"
	"errors"
	"fmt"
	"strconv"
	"strings"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/util/intstr"
	"k8s.io/apimachinery/pkg/util/validation"

	"github.com/datawire/ambassador/pkg/kates"
)

// svcPortByNameOrNumber iterates through a list of ports in a service and
// only returns the ports that match the given nameOrNumber
func svcPortByNameOrNumber(svc *kates.Service, nameOrNumber string) []*kates.ServicePort {
	svcPorts := make([]*kates.ServicePort, 0)
	ports := svc.Spec.Ports
	var isName bool
	validName := validation.IsValidPortName(nameOrNumber)
	if len(validName) > 0 {
		isName = false
	} else {
		isName = true
	}
	for i := range ports {
		port := &ports[i]
		matchFound := false
		// If no nameOrNumber has been specified, we include it
		if nameOrNumber == "" {
			matchFound = true
		}
		// If the nameOrNumber is a valid name, we compare it to the
		// name listed in the servicePort
		if isName {
			if nameOrNumber == port.Name {
				matchFound = true
			}
		} else {
			// Otherwise we compare it to the port number
			givenPort, err := strconv.Atoi(nameOrNumber)
			if err == nil && int32(givenPort) == port.Port {
				matchFound = true
			}
		}
		if matchFound {
			svcPorts = append(svcPorts, port)
		}
	}
	return svcPorts
}

func FindMatchingServices(c context.Context, client *kates.Client, portNameOrNumber, svcName, namespace string, labels map[string]string) ([]*kates.Service, error) {
	// TODO: Expensive on large clusters but the problem goes away once we move the installer to the traffic-manager
	var svcs []*kates.Service
	if err := client.List(c, kates.Query{Name: svcName, Kind: "Service", Namespace: namespace}, &svcs); err != nil {
		return nil, err
	}

	// Returns true if selector is completely included in labels
	labelsMatch := func(selector map[string]string) bool {
		if len(selector) == 0 || len(labels) < len(selector) {
			return false
		}
		for k, v := range selector {
			if labels[k] != v {
				return false
			}
		}
		return true
	}

	var matching []*kates.Service
	for _, svc := range svcs {
		if (svcName == "" || svc.Name == svcName) && labelsMatch(svc.Spec.Selector) && len(svcPortByNameOrNumber(svc, portNameOrNumber)) > 0 {
			matching = append(matching, svc)
		}
	}
	return matching, nil
}

func FindMatchingService(c context.Context, client *kates.Client, portNameOrNumber, svcName, namespace string, labels map[string]string) (*kates.Service, error) {
	matchingSvcs, err := FindMatchingServices(c, client, portNameOrNumber, svcName, namespace, labels)
	if err != nil {
		return nil, err
	}
	if len(matchingSvcs) == 1 {
		return matchingSvcs[0], nil
	}

	count := "no"
	suffix := ""
	portRef := ""
	if len(matchingSvcs) > 0 {
		svcNames := make([]string, len(matchingSvcs))
		for i, svc := range matchingSvcs {
			svcNames[i] = svc.Name
		}
		count = "multiple"
		suffix = fmt.Sprintf(", use --service and one of: %s", strings.Join(svcNames, ","))
	}
	if portNameOrNumber != "" {
		portRef = fmt.Sprintf(" and a port referenced by name or port number %s", portNameOrNumber)
	}
	return nil, fmt.Errorf("found %s services with a selector matching labels %v%s in namespace %s%s", count, labels, portRef, namespace, suffix)
}

// FindMatchingPort finds the matching container associated with portNameOrNumber
// in the given service.
func FindMatchingPort(cns []corev1.Container, portNameOrNumber string, svc *kates.Service) (
	sPort *kates.ServicePort,
	cn *kates.Container,
	cPortIndex int,
	err error,
) {
	// For now, we only support intercepting one port on a given service.
	ports := svcPortByNameOrNumber(svc, portNameOrNumber)
	switch numPorts := len(ports); {
	case numPorts == 0:
		// this may happen when portNameOrNumber is specified but none of the
		// ports match
		return nil, nil, 0, errors.New("found no Service with a port that matches any container in this workload")

	case numPorts > 1:
		return nil, nil, 0, errors.New(`found matching Service with multiple matching ports.
Please specify the Service port you want to intercept by passing the --port=local:svcPortName flag.`)
	default:
	}
	port := ports[0]
	var matchingServicePort *corev1.ServicePort
	var matchingContainer *corev1.Container
	var containerPortIndex int

	if port.TargetPort.Type == intstr.String {
		portName := port.TargetPort.StrVal
		for ci := 0; ci < len(cns) && matchingContainer == nil; ci++ {
			cn := &cns[ci]
			for pi := range cn.Ports {
				if cn.Ports[pi].Name == portName {
					matchingServicePort = port
					matchingContainer = cn
					containerPortIndex = pi
					break
				}
			}
		}
	} else {
		portNum := port.TargetPort.IntVal
		// First see if we have a container with a matching port
	containerLoop:
		for ci := range cns {
			cn := &cns[ci]
			for pi := range cn.Ports {
				if cn.Ports[pi].ContainerPort == portNum {
					matchingServicePort = port
					matchingContainer = cn
					containerPortIndex = pi
					break
				}
			}
		}
		// If no container matched, then use the first container with no ports at all. This
		// enables intercepts of containers that indeed do listen a port but lack a matching
		// port description in the manifest, which is what you get if you do:
		//     kubectl create deploy my-deploy --image my-image
		//     kubectl expose deploy my-deploy --port 80 --target-port 8080
		if matchingContainer == nil {
			for ci := range cns {
				cn := &cns[ci]
				if len(cn.Ports) == 0 {
					matchingServicePort = port
					matchingContainer = cn
					containerPortIndex = -1
					break
				}
			}
		}
	}

	if matchingServicePort == nil {
		return nil, nil, 0, errors.New("found no Service with a port that matches any container in this workload")
	}
	return matchingServicePort, matchingContainer, containerPortIndex, nil
}
<|endoftext|>


<commit_before>// Copyright © 2021 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 minikube

import (
	"fmt"
	"os"
	"os/exec"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"time"

	"knative.dev/kn-plugin-quickstart/pkg/install"
)

var clusterName string
var kubernetesVersion = "1.22.4"
var minikubeVersion = 1.23

// SetUp creates a local Minikube cluster and installs all the relevant Knative components
func SetUp(name string) error {
	start := time.Now()
	clusterName = name

	if err := createMinikubeCluster(); err != nil {
		return fmt.Errorf("creating cluster: %w", err)
	}
	if runtime.GOOS == "darwin" || runtime.GOOS == "windows" {
		fmt.Print("\n")
		fmt.Println("To finish setting up networking for minikube, run the following command in a separate terminal window:")
		fmt.Println("    minikube tunnel --profile knative")
		fmt.Println("\nPress the Enter key to continue")
		fmt.Scanln()
	}
	if err := install.Serving(); err != nil {
		return fmt.Errorf("install serving: %w", err)
	}
	if err := install.Kourier(); err != nil {
		return fmt.Errorf("install kourier: %w", err)
	}
	if err := install.KourierMinikube(); err != nil {
		return fmt.Errorf("configure kourier: %w", err)
	}
	if err := install.Eventing(); err != nil {
		return fmt.Errorf("install eventing: %w", err)
	}

	finish := time.Since(start).Round(time.Second)
	fmt.Printf("🚀 Knative install took: %s \n", finish)
	fmt.Println("🎉 Now have some fun with Serverless and Event Driven Apps!")

	return nil
}

func createMinikubeCluster() error {
	if err := checkMinikubeVersion(); err != nil {
		return fmt.Errorf("minikube version: %w", err)
	}
	if err := checkForExistingCluster(); err != nil {
		return fmt.Errorf("existing cluster: %w", err)
	}
	return nil
}

// checkMinikubeVersion validates that the user has the correct version of Minikube installed.
// If not, it prompts the user to download a newer version before continuing.
func checkMinikubeVersion() error {
	versionCheck := exec.Command("minikube", "version", "--short")
	out, err := versionCheck.CombinedOutput()
	if err != nil {
		return fmt.Errorf("minikube version: %w", err)
	}
	fmt.Printf("Minikube version is: %s\n", string(out))

	userMinikubeVersion, err := parseMinikubeVersion(string(out))
	if err != nil {
		return fmt.Errorf("parsing minikube version: %w", err)
	}
	if userMinikubeVersion < minikubeVersion {
		var resp string
		fmt.Printf("WARNING: We require at least Minikube v%.2f, while you are using v%.2f\n", minikubeVersion, userMinikubeVersion)
		fmt.Println("You can download a newer version from https://github.com/kubernetes/minikube/releases/")
		fmt.Print("Continue anyway? (not recommended) [y/N]: ")
		fmt.Scanf("%s", &resp)
		if strings.ToLower(resp) != "y" {
			fmt.Println("Installation stopped. Please upgrade minikube and run again")
			os.Exit(0)
		}
	}

	return nil
}

// checkForExistingCluster checks if the user already has a Minikube cluster. If so, it provides
// the option of deleting the existing cluster and recreating it. If not, it proceeds to
// creating a new cluster
func checkForExistingCluster() error {
	getClusters := exec.Command("minikube", "profile", "list")
	out, err := getClusters.CombinedOutput()
	if err != nil {
		// there are no existing minikube profiles, the listing profiles command will error
		// if there were no profiles, we simply want to create a new one and not stop the install
		// so if the error is the "MK_USAGE_NO_PROFILE" error, we ignore it and continue onwards
		if !strings.Contains(string(out), "MK_USAGE_NO_PROFILE") {
			return fmt.Errorf("check cluster: %w", err)
		}
	}
	// TODO Add tests for regex
	r := regexp.MustCompile(clusterName)
	matches := r.Match(out)
	if matches {
		var resp string
		fmt.Print("Knative Cluster " + clusterName + " already installed.\nDelete and recreate [y/N]: ")
		fmt.Scanf("%s", &resp)
		if strings.ToLower(resp) != "y" {
			fmt.Println("Installation skipped")
			return nil
		}
		fmt.Println("deleting cluster...")
		deleteCluster := exec.Command("minikube", "delete", "--profile", clusterName)
		if err := deleteCluster.Run(); err != nil {
			return fmt.Errorf("delete cluster: %w", err)
		}
		if err := createNewCluster(); err != nil {
			return fmt.Errorf("new cluster: %w", err)
		}
		return nil
	}

	if err := createNewCluster(); err != nil {
		return fmt.Errorf("new cluster: %w", err)
	}

	return nil
}

// createNewCluster creates a new Minikube cluster
func createNewCluster() error {
	fmt.Println("☸ Creating Minikube cluster...")
	fmt.Println("\nBy default, using the standard minikube driver for your system")
	fmt.Println("If you wish to use a different driver, please configure minikube using")
	fmt.Print("    minikube config set driver <your-driver>\n\n")

	// create cluster and wait until ready
	createCluster := exec.Command("minikube", "start", "--kubernetes-version", kubernetesVersion, "--cpus", "3", "--profile", clusterName, "--wait", "all")
	if err := runCommandWithOutput(createCluster); err != nil {
		return fmt.Errorf("minikube create: %w", err)
	}

	return nil
}

func runCommandWithOutput(c *exec.Cmd) error {
	c.Stdout = os.Stdout
	c.Stderr = os.Stderr
	if err := c.Run(); err != nil {
		return fmt.Errorf("piping output: %w", err)
	}
	fmt.Print("\n")
	return nil
}

func parseMinikubeVersion(v string) (float64, error) {
	strippedVersion := strings.TrimLeft(strings.TrimRight(v, "\n"), "v")
	dotVersion := strings.Split(strippedVersion, ".")
	floatVersion, err := strconv.ParseFloat(dotVersion[0]+"."+dotVersion[1], 64)
	if err != nil {
		return 0, err
	}

	return floatVersion, nil
}
<commit_msg>add instruction to keep minikube tunnel terminal open (#188)<commit_after>// Copyright © 2021 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 minikube

import (
	"fmt"
	"os"
	"os/exec"
	"regexp"
	"runtime"
	"strconv"
	"strings"
	"time"

	"knative.dev/kn-plugin-quickstart/pkg/install"
)

var clusterName string
var kubernetesVersion = "1.22.4"
var minikubeVersion = 1.23

// SetUp creates a local Minikube cluster and installs all the relevant Knative components
func SetUp(name string) error {
	start := time.Now()
	clusterName = name

	if err := createMinikubeCluster(); err != nil {
		return fmt.Errorf("creating cluster: %w", err)
	}
	if runtime.GOOS == "darwin" || runtime.GOOS == "windows" {
		fmt.Print("\n")
		fmt.Println("To finish setting up networking for minikube, run the following command in a separate terminal window:")
		fmt.Println("    minikube tunnel --profile knative")
		fmt.Println("The tunnel window must remain open until you are done with the quickstart environment.")
		fmt.Println("\nPress the Enter key to continue")
		fmt.Scanln()
	}
	if err := install.Serving(); err != nil {
		return fmt.Errorf("install serving: %w", err)
	}
	if err := install.Kourier(); err != nil {
		return fmt.Errorf("install kourier: %w", err)
	}
	if err := install.KourierMinikube(); err != nil {
		return fmt.Errorf("configure kourier: %w", err)
	}
	if err := install.Eventing(); err != nil {
		return fmt.Errorf("install eventing: %w", err)
	}

	finish := time.Since(start).Round(time.Second)
	fmt.Printf("🚀 Knative install took: %s \n", finish)
	fmt.Println("🎉 Now have some fun with Serverless and Event Driven Apps!")

	return nil
}

func createMinikubeCluster() error {
	if err := checkMinikubeVersion(); err != nil {
		return fmt.Errorf("minikube version: %w", err)
	}
	if err := checkForExistingCluster(); err != nil {
		return fmt.Errorf("existing cluster: %w", err)
	}
	return nil
}

// checkMinikubeVersion validates that the user has the correct version of Minikube installed.
// If not, it prompts the user to download a newer version before continuing.
func checkMinikubeVersion() error {
	versionCheck := exec.Command("minikube", "version", "--short")
	out, err := versionCheck.CombinedOutput()
	if err != nil {
		return fmt.Errorf("minikube version: %w", err)
	}
	fmt.Printf("Minikube version is: %s\n", string(out))

	userMinikubeVersion, err := parseMinikubeVersion(string(out))
	if err != nil {
		return fmt.Errorf("parsing minikube version: %w", err)
	}
	if userMinikubeVersion < minikubeVersion {
		var resp string
		fmt.Printf("WARNING: We require at least Minikube v%.2f, while you are using v%.2f\n", minikubeVersion, userMinikubeVersion)
		fmt.Println("You can download a newer version from https://github.com/kubernetes/minikube/releases/")
		fmt.Print("Continue anyway? (not recommended) [y/N]: ")
		fmt.Scanf("%s", &resp)
		if strings.ToLower(resp) != "y" {
			fmt.Println("Installation stopped. Please upgrade minikube and run again")
			os.Exit(0)
		}
	}

	return nil
}

// checkForExistingCluster checks if the user already has a Minikube cluster. If so, it provides
// the option of deleting the existing cluster and recreating it. If not, it proceeds to
// creating a new cluster
func checkForExistingCluster() error {
	getClusters := exec.Command("minikube", "profile", "list")
	out, err := getClusters.CombinedOutput()
	if err != nil {
		// there are no existing minikube profiles, the listing profiles command will error
		// if there were no profiles, we simply want to create a new one and not stop the install
		// so if the error is the "MK_USAGE_NO_PROFILE" error, we ignore it and continue onwards
		if !strings.Contains(string(out), "MK_USAGE_NO_PROFILE") {
			return fmt.Errorf("check cluster: %w", err)
		}
	}
	// TODO Add tests for regex
	r := regexp.MustCompile(clusterName)
	matches := r.Match(out)
	if matches {
		var resp string
		fmt.Print("Knative Cluster " + clusterName + " already installed.\nDelete and recreate [y/N]: ")
		fmt.Scanf("%s", &resp)
		if strings.ToLower(resp) != "y" {
			fmt.Println("Installation skipped")
			return nil
		}
		fmt.Println("deleting cluster...")
		deleteCluster := exec.Command("minikube", "delete", "--profile", clusterName)
		if err := deleteCluster.Run(); err != nil {
			return fmt.Errorf("delete cluster: %w", err)
		}
		if err := createNewCluster(); err != nil {
			return fmt.Errorf("new cluster: %w", err)
		}
		return nil
	}

	if err := createNewCluster(); err != nil {
		return fmt.Errorf("new cluster: %w", err)
	}

	return nil
}

// createNewCluster creates a new Minikube cluster
func createNewCluster() error {
	fmt.Println("☸ Creating Minikube cluster...")
	fmt.Println("\nBy default, using the standard minikube driver for your system")
	fmt.Println("If you wish to use a different driver, please configure minikube using")
	fmt.Print("    minikube config set driver <your-driver>\n\n")

	// create cluster and wait until ready
	createCluster := exec.Command("minikube", "start", "--kubernetes-version", kubernetesVersion, "--cpus", "3", "--profile", clusterName, "--wait", "all")
	if err := runCommandWithOutput(createCluster); err != nil {
		return fmt.Errorf("minikube create: %w", err)
	}

	return nil
}

func runCommandWithOutput(c *exec.Cmd) error {
	c.Stdout = os.Stdout
	c.Stderr = os.Stderr
	if err := c.Run(); err != nil {
		return fmt.Errorf("piping output: %w", err)
	}
	fmt.Print("\n")
	return nil
}

func parseMinikubeVersion(v string) (float64, error) {
	strippedVersion := strings.TrimLeft(strings.TrimRight(v, "\n"), "v")
	dotVersion := strings.Split(strippedVersion, ".")
	floatVersion, err := strconv.ParseFloat(dotVersion[0]+"."+dotVersion[1], 64)
	if err != nil {
		return 0, err
	}

	return floatVersion, nil
}
<|endoftext|>