<commit_before>/*
Copyright 2019 The xridge kubestone contributors.

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

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

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

package k8s

import (
	"context"
	"fmt"

	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	k8sclient "k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/record"
	"k8s.io/client-go/tools/reference"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

// Access provides client related structs to access kubernetes
type Access struct {
	Client        client.Client
	Clientset     *k8sclient.Clientset
	Scheme        *runtime.Scheme
	EventRecorder record.EventRecorder
}

// CreateWithReference method creates a kubernetes resource and
// sets the owner reference to a given object. It provides basic
// idempotency (by ignoring Already Exists errors).
// Successful creation of the event is logged via EventRecorder
// to the owner.
func (a *Access) CreateWithReference(ctx context.Context, object, owner metav1.Object) error {
	runtimeObject, ok := object.(runtime.Object)
	if !ok {
		return fmt.Errorf("object (%T) is not a runtime.Object", object)
	}

	runtimeOwner, ok := owner.(runtime.Object)
	if !ok {
		return fmt.Errorf("owner (%T) is not a runtime.Object", object)
	}

	ownerRef, err := reference.GetReference(a.Scheme, runtimeOwner)
	if err != nil {
		return fmt.Errorf("Unable to get reference to owner")
	}

	if err := controllerutil.SetControllerReference(owner, object, a.Scheme); err != nil {
		return err
	}

	err = a.Client.Create(ctx, runtimeObject)
	if IgnoreAlreadyExists(err) != nil {
		return err
	}

	if !errors.IsAlreadyExists(err) {
		a.EventRecorder.Eventf(ownerRef, corev1.EventTypeNormal, CreateSucceeded,
			"Created %v", object.GetSelfLink())
	}

	return nil
}

// DeleteObject method deletes a kubernetes resource while
// ignores not found errors, so that it can be called multiple times.
// Successful deletion of the event is logged via EventRecorder
// to the owner.
func (a *Access) DeleteObject(ctx context.Context, object, owner metav1.Object) error {
	runtimeObject, ok := object.(runtime.Object)
	if !ok {
		return fmt.Errorf("object (%T) is not a runtime.Object", object)
	}

	runtimeOwner, ok := owner.(runtime.Object)
	if !ok {
		return fmt.Errorf("owner (%T) is not a runtime.Object", object)
	}

	ownerRef, err := reference.GetReference(a.Scheme, runtimeOwner)
	if err != nil {
		return fmt.Errorf("Unable to get reference to owner")
	}

	// Need to get the object first so that the object.GetSelfLink()
	// works during Event Recording
	namespacedName := types.NamespacedName{
		Namespace: object.GetNamespace(),
		Name:      object.GetName(),
	}
	err = a.Client.Get(ctx, namespacedName, runtimeObject)
	if IgnoreNotFound(err) != nil {
		return err
	} else if errors.IsNotFound(err) {
		return nil
	}

	err = a.Client.Delete(ctx, runtimeObject)
	if IgnoreNotFound(err) != nil {
		return err
	}

	if !errors.IsNotFound(err) {
		a.EventRecorder.Eventf(ownerRef, corev1.EventTypeNormal, DeleteSucceeded,
			"Deleted %v", object.GetSelfLink())
	}

	return nil
}

// IsJobFinished returns true if the given job has already succeeded or failed
func (a *Access) IsJobFinished(namespacedName types.NamespacedName) (finished bool, err error) {
	job, err := a.Clientset.BatchV1().Jobs(namespacedName.Namespace).Get(
		namespacedName.Name, metav1.GetOptions{})
	if err != nil {
		return false, err
	}

	finished = job.Status.CompletionTime != nil
	return finished, nil
}

// IsEndpointReady returns true if the given endpoint is fully connected to at least one pod
func (a *Access) IsEndpointReady(namespacedName types.NamespacedName) (finished bool, err error) {
	// The Endpoint connection between the Service and the Pod is the final step before
	// a service becomes reachable in Kubernetes. When the endpoint is bound, your
	// service becomes connectable on vanilla k8s and azure, but not on GKE.
	// For details see #96: https://github.com/xridge/kubestone/issues/96
	//
	// Even though it is not enough to wait for the endpoints in certain cloud providers,
	// it is still the closest we can get between service creation and connectibility.
	endpoint, err := a.Clientset.CoreV1().Endpoints(namespacedName.Namespace).Get(
		namespacedName.Name, metav1.GetOptions{})
	if err != nil {
		return false, err
	}

	readyAddresses := 0
	for _, subset := range endpoint.Subsets {
		if len(subset.NotReadyAddresses) > 0 {
			return false, nil
		}
		readyAddresses += len(subset.Addresses)
	}

	ready := readyAddresses > 0

	return ready, nil
}

// IsDeploymentReady returns true if the given deployment's ready replicas matching with the desired replicas
func (a *Access) IsDeploymentReady(namespacedName types.NamespacedName) (ready bool, err error) {
	ready, err = false, nil
	deployment, err := a.Clientset.AppsV1().Deployments(namespacedName.Namespace).Get(
		namespacedName.Name, metav1.GetOptions{})
	if err != nil {
		return ready, err
	}

	ready = deployment.Status.ReadyReplicas == *deployment.Spec.Replicas

	return ready, err
}
<commit_msg>FIX: Add RBAC rules to get and list endpoints<commit_after>/*
Copyright 2019 The xridge kubestone contributors.

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

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

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

package k8s

import (
	"context"
	"fmt"

	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	k8sclient "k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/record"
	"k8s.io/client-go/tools/reference"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

// Access provides client related structs to access kubernetes
type Access struct {
	Client        client.Client
	Clientset     *k8sclient.Clientset
	Scheme        *runtime.Scheme
	EventRecorder record.EventRecorder
}

// CreateWithReference method creates a kubernetes resource and
// sets the owner reference to a given object. It provides basic
// idempotency (by ignoring Already Exists errors).
// Successful creation of the event is logged via EventRecorder
// to the owner.
func (a *Access) CreateWithReference(ctx context.Context, object, owner metav1.Object) error {
	runtimeObject, ok := object.(runtime.Object)
	if !ok {
		return fmt.Errorf("object (%T) is not a runtime.Object", object)
	}

	runtimeOwner, ok := owner.(runtime.Object)
	if !ok {
		return fmt.Errorf("owner (%T) is not a runtime.Object", object)
	}

	ownerRef, err := reference.GetReference(a.Scheme, runtimeOwner)
	if err != nil {
		return fmt.Errorf("Unable to get reference to owner")
	}

	if err := controllerutil.SetControllerReference(owner, object, a.Scheme); err != nil {
		return err
	}

	err = a.Client.Create(ctx, runtimeObject)
	if IgnoreAlreadyExists(err) != nil {
		return err
	}

	if !errors.IsAlreadyExists(err) {
		a.EventRecorder.Eventf(ownerRef, corev1.EventTypeNormal, CreateSucceeded,
			"Created %v", object.GetSelfLink())
	}

	return nil
}

// DeleteObject method deletes a kubernetes resource while
// ignores not found errors, so that it can be called multiple times.
// Successful deletion of the event is logged via EventRecorder
// to the owner.
func (a *Access) DeleteObject(ctx context.Context, object, owner metav1.Object) error {
	runtimeObject, ok := object.(runtime.Object)
	if !ok {
		return fmt.Errorf("object (%T) is not a runtime.Object", object)
	}

	runtimeOwner, ok := owner.(runtime.Object)
	if !ok {
		return fmt.Errorf("owner (%T) is not a runtime.Object", object)
	}

	ownerRef, err := reference.GetReference(a.Scheme, runtimeOwner)
	if err != nil {
		return fmt.Errorf("Unable to get reference to owner")
	}

	// Need to get the object first so that the object.GetSelfLink()
	// works during Event Recording
	namespacedName := types.NamespacedName{
		Namespace: object.GetNamespace(),
		Name:      object.GetName(),
	}
	err = a.Client.Get(ctx, namespacedName, runtimeObject)
	if IgnoreNotFound(err) != nil {
		return err
	} else if errors.IsNotFound(err) {
		return nil
	}

	err = a.Client.Delete(ctx, runtimeObject)
	if IgnoreNotFound(err) != nil {
		return err
	}

	if !errors.IsNotFound(err) {
		a.EventRecorder.Eventf(ownerRef, corev1.EventTypeNormal, DeleteSucceeded,
			"Deleted %v", object.GetSelfLink())
	}

	return nil
}

// IsJobFinished returns true if the given job has already succeeded or failed
func (a *Access) IsJobFinished(namespacedName types.NamespacedName) (finished bool, err error) {
	job, err := a.Clientset.BatchV1().Jobs(namespacedName.Namespace).Get(
		namespacedName.Name, metav1.GetOptions{})
	if err != nil {
		return false, err
	}

	finished = job.Status.CompletionTime != nil
	return finished, nil
}

// +kubebuilder:rbac:groups="",resources=endpoints,verbs=get;list

// IsEndpointReady returns true if the given endpoint is fully connected to at least one pod
func (a *Access) IsEndpointReady(namespacedName types.NamespacedName) (finished bool, err error) {
	// The Endpoint connection between the Service and the Pod is the final step before
	// a service becomes reachable in Kubernetes. When the endpoint is bound, your
	// service becomes connectable on vanilla k8s and azure, but not on GKE.
	// For details see #96: https://github.com/xridge/kubestone/issues/96
	//
	// Even though it is not enough to wait for the endpoints in certain cloud providers,
	// it is still the closest we can get between service creation and connectibility.
	endpoint, err := a.Clientset.CoreV1().Endpoints(namespacedName.Namespace).Get(
		namespacedName.Name, metav1.GetOptions{})
	if err != nil {
		return false, err
	}

	readyAddresses := 0
	for _, subset := range endpoint.Subsets {
		if len(subset.NotReadyAddresses) > 0 {
			return false, nil
		}
		readyAddresses += len(subset.Addresses)
	}

	ready := readyAddresses > 0

	return ready, nil
}

// IsDeploymentReady returns true if the given deployment's ready replicas matching with the desired replicas
func (a *Access) IsDeploymentReady(namespacedName types.NamespacedName) (ready bool, err error) {
	ready, err = false, nil
	deployment, err := a.Clientset.AppsV1().Deployments(namespacedName.Namespace).Get(
		namespacedName.Name, metav1.GetOptions{})
	if err != nil {
		return ready, err
	}

	ready = deployment.Status.ReadyReplicas == *deployment.Spec.Replicas

	return ready, err
}
<|endoftext|>


<commit_before>// Copyright 2015 Google Inc.
//
// 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 yang

// This file implements Parse, which  parses the input as generic YANG and
// returns a slice of base Statements (which in turn may contain more
// Statements, i.e., a slice of Statement trees.)
//
// TODO(borman): remove this TODO once ast.go is part of of this package.
// See ast.go for the conversion of Statements into an AST tree of Nodes.

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"strings"
)

// a parser is used to parse the contents of a single .yang file.
type parser struct {
	lex    *lexer
	errout *bytes.Buffer
	tokens []*token // stack of pushed tokens (for backing up)

	statementDepth int

	// hitBrace is returned when we encounter a '}'.  The statement location
	// is updated with the location of the '}'.  The brace may be legitimate
	// but only the caller will know if it is.  That is, the brace may be
	// closing our parent or may be an error (we didn't expect it).
	// hitBrace is updated with the file, line, and column of the brace's
	// location.
	hitBrace *Statement
}

// A Statement is a generic YANG statement.  A Statement may have optional
// sub-statement (i.e., a Statement is a tree).
type Statement struct {
	Keyword     string
	HasArgument bool
	Argument    string
	statements  []*Statement

	file string
	line int // 1's based line number
	col  int // 1's based column number
}

// FakeStatement returns a statement filled in with keyword, file, line and col.
func FakeStatement(keyword, file string, line, col int) *Statement {
	return &Statement{
		Keyword: keyword,
		file:    file,
		line:    line,
		col:     col,
	}
}

func (s *Statement) NName() string         { return s.Argument }
func (s *Statement) Kind() string          { return s.Keyword }
func (s *Statement) Statement() *Statement { return s }
func (s *Statement) ParentNode() Node      { return nil }
func (s *Statement) Exts() []*Statement    { return nil }

// Arg returns the optional argument to s.  It returns false if s has no
// argument.
func (s *Statement) Arg() (string, bool) { return s.Argument, s.HasArgument }

// Keyword returns the keyword of s.
//func (s *Statement) Keyword() string { return s.Keyword }

// SubStatements returns a slice of Statements found in s.
func (s *Statement) SubStatements() []*Statement { return s.statements }

// String returns s's tree as a string.
func (s *Statement) String() string {
	var b bytes.Buffer
	s.Write(&b, "")
	return b.String()
}

// Location returns the location in the source where s was defined.
func (s *Statement) Location() string {
	switch {
	case s.file == "" && s.line == 0:
		return "unknown"
	case s.file == "":
		return fmt.Sprintf("line %d:%d", s.line, s.col)
	case s.line == 0:
		return s.file
	default:
		return fmt.Sprintf("%s:%d:%d", s.file, s.line, s.col)
	}
}

// Write writes the tree in s to w, each line indented by ident.  Children
// nodes are indented further by a tab.  Typically indent is "" at the top
// level.  Write is intended to display the contents of Statement, but
// not necessarily reproduce the input of Statement.
func (s *Statement) Write(w io.Writer, indent string) error {
	if s.Keyword == "" {
		// We are just a collection of statements at the top level.
		for _, s := range s.statements {
			if err := s.Write(w, indent); err != nil {
				return err
			}
		}
		return nil
	}

	parts := []string{fmt.Sprintf("%s%s", indent, s.Keyword)}
	if s.HasArgument {
		args := strings.Split(s.Argument, "\n")
		if len(args) == 1 {
			parts = append(parts, fmt.Sprintf(" %q", s.Argument))
		} else {
			parts = append(parts, ` "`, args[0], "\n")
			i := fmt.Sprintf("%*s", len(s.Keyword)+1, "")
			for x, p := range args[1:] {
				s := fmt.Sprintf("%q", p)
				s = s[1 : len(s)-1]
				parts = append(parts, indent, " ", i, s)
				if x == len(args[1:])-1 {
					// last part just needs the closing "
					parts = append(parts, `"`)
				} else {
					parts = append(parts, "\n")
				}
			}
		}
	}

	if len(s.statements) == 0 {
		_, err := fmt.Fprintf(w, "%s;\n", strings.Join(parts, ""))
		return err
	}
	if _, err := fmt.Fprintf(w, "%s {\n", strings.Join(parts, "")); err != nil {
		return err
	}
	for _, s := range s.statements {
		if err := s.Write(w, indent+"\t"); err != nil {
			return err
		}
	}
	if _, err := fmt.Fprintf(w, "%s}\n", indent); err != nil {
		return err
	}
	return nil
}

// ignoreMe is returned to continue processing after an error (the parse will
// fail, but we want to look for more errors).
var ignoreMe = &Statement{}

// Parse parses the input as generic YANG and returns the statements parsed.
// The path parameter should be the source name where input was read from (e.g.,
// the file name the input was read from).  If one more more errors are
// encountered, nil and an error are returned.  The error's text includes all
// errors encountered.
func Parse(input, path string) ([]*Statement, error) {
	var statements []*Statement
	p := &parser{
		lex:      newLexer(input, path),
		errout:   &bytes.Buffer{},
		hitBrace: &Statement{},
	}
	p.lex.errout = p.errout
Loop:
	for {
		switch ns := p.nextStatement(); ns {
		case nil:
			break Loop
		case p.hitBrace:
			fmt.Fprintf(p.errout, "%s:%d:%d: unexpected %c\n", ns.file, ns.line, ns.col, closeBrace)
		default:
			statements = append(statements, ns)
		}
	}

	p.checkStatementDepth()

	if p.errout.Len() == 0 {
		return statements, nil
	}
	return nil, errors.New(strings.TrimSpace(p.errout.String()))

}

// push pushes tokens t back on the input stream so they will be the next
// tokens returned by next.  The tokens list is a LIFO so the final token
// listed to push will be the next token returned.
func (p *parser) push(t ...*token) {
	p.tokens = append(p.tokens, t...)
}

// pop returns the last token pushed, or nil if the token stack is empty.
func (p *parser) pop() *token {
	if n := len(p.tokens); n > 0 {
		n--
		defer func() { p.tokens = p.tokens[:n] }()
		return p.tokens[n]
	}
	return nil
}

// next returns the next token from the lexer.  It also handles string
// concatenation.
func (p *parser) next() *token {
	if t := p.pop(); t != nil {
		return t
	}
	next := func() *token {
		for {
			if t := p.lex.NextToken(); t.Code() != tError {
				return t
			}
		}
	}
	t := next()
	if t.Code() != tString {
		return t
	}
	// Handle `"string" + "string"`
	for {
		nt := next()
		switch nt.Code() {
		case tEOF:
			return t
		case tIdentifier:
			if nt.Text != "+" {
				p.push(nt)
				return t
			}
		default:
			p.push(nt)
			return t
		}
		// We found a +, now look for a following string
		st := next()
		switch st.Code() {
		case tEOF:
			p.push(nt)
			return t
		case tString:
			// concatenate the text and drop the nt and st tokens
			// try again
			t.Text += st.Text
		default:
			p.push(st, nt)
			return t
		}

	}
}

// nextStatement returns the next statement in the input, which may in turn
// recurse to read sub statements.
func (p *parser) nextStatement() *Statement {
	t := p.next()
	switch t.Code() {
	case tEOF:
		return nil
	case closeBrace:
		p.statementDepth -= 1
		p.hitBrace.file = t.File
		p.hitBrace.line = t.Line
		p.hitBrace.col = t.Col
		return p.hitBrace
	case tIdentifier:
	default:
		fmt.Fprintf(p.errout, "%v: not an identifier\n", t)
		return ignoreMe
	}

	s := &Statement{
		Keyword: t.Text,
		file:    t.File,
		line:    t.Line,
		col:     t.Col,
	}

	// The keyword "pattern" must be treated special.  When
	// parsing the argument for "pattern", escape sequences
	// must be expanded differently.
	p.lex.inPattern = t.Text == "pattern"
	t = p.next()
	p.lex.inPattern = false
	switch t.Code() {
	case tString, tIdentifier:
		s.HasArgument = true
		s.Argument = t.Text
		t = p.next()
	}
	switch t.Code() {
	case tEOF:
		fmt.Fprintf(p.errout, "%s: unexpected EOF\n", s.file)
		return nil
	case ';':
		return s
	case openBrace:
		p.statementDepth += 1
		for {
			switch ns := p.nextStatement(); ns {
			case nil:
				return nil
			case p.hitBrace:
				return s
			default:
				s.statements = append(s.statements, ns)
			}
		}
	default:
		fmt.Fprintf(p.errout, "%v: syntax error\n", t)
		return ignoreMe
	}
}

// Checks that we have a statement depth of 0. It's an error to exit
// the parser with a depth of > 0, it means we are missing closing
// braces. Note: the parser will error out for the case where we
// start with an unmatched close brace
//
// This test is only done if there are no other errors as
// we may exit early due to those errors -- and therefore there *might*
// not really be a mismatched brace issue.
func (p *parser) checkStatementDepth() {
	// don't check if there are other errors
	if p.errout.Len() > 0 {
		return
	}
	if p.statementDepth > 0 {
		plural := ""
		if p.statementDepth > 1 {
			plural = "s"
		}
		fmt.Fprintf(p.errout, "%s:%d:%d: missing %d closing brace%s\n",
			p.lex.file, p.lex.line, p.lex.col, p.statementDepth, plural)
	}
}
<commit_msg>Reviewer comments for early return<commit_after>// Copyright 2015 Google Inc.
//
// 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 yang

// This file implements Parse, which  parses the input as generic YANG and
// returns a slice of base Statements (which in turn may contain more
// Statements, i.e., a slice of Statement trees.)
//
// TODO(borman): remove this TODO once ast.go is part of of this package.
// See ast.go for the conversion of Statements into an AST tree of Nodes.

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"strings"
)

// a parser is used to parse the contents of a single .yang file.
type parser struct {
	lex    *lexer
	errout *bytes.Buffer
	tokens []*token // stack of pushed tokens (for backing up)

	statementDepth int

	// hitBrace is returned when we encounter a '}'.  The statement location
	// is updated with the location of the '}'.  The brace may be legitimate
	// but only the caller will know if it is.  That is, the brace may be
	// closing our parent or may be an error (we didn't expect it).
	// hitBrace is updated with the file, line, and column of the brace's
	// location.
	hitBrace *Statement
}

// A Statement is a generic YANG statement.  A Statement may have optional
// sub-statement (i.e., a Statement is a tree).
type Statement struct {
	Keyword     string
	HasArgument bool
	Argument    string
	statements  []*Statement

	file string
	line int // 1's based line number
	col  int // 1's based column number
}

// FakeStatement returns a statement filled in with keyword, file, line and col.
func FakeStatement(keyword, file string, line, col int) *Statement {
	return &Statement{
		Keyword: keyword,
		file:    file,
		line:    line,
		col:     col,
	}
}

func (s *Statement) NName() string         { return s.Argument }
func (s *Statement) Kind() string          { return s.Keyword }
func (s *Statement) Statement() *Statement { return s }
func (s *Statement) ParentNode() Node      { return nil }
func (s *Statement) Exts() []*Statement    { return nil }

// Arg returns the optional argument to s.  It returns false if s has no
// argument.
func (s *Statement) Arg() (string, bool) { return s.Argument, s.HasArgument }

// Keyword returns the keyword of s.
//func (s *Statement) Keyword() string { return s.Keyword }

// SubStatements returns a slice of Statements found in s.
func (s *Statement) SubStatements() []*Statement { return s.statements }

// String returns s's tree as a string.
func (s *Statement) String() string {
	var b bytes.Buffer
	s.Write(&b, "")
	return b.String()
}

// Location returns the location in the source where s was defined.
func (s *Statement) Location() string {
	switch {
	case s.file == "" && s.line == 0:
		return "unknown"
	case s.file == "":
		return fmt.Sprintf("line %d:%d", s.line, s.col)
	case s.line == 0:
		return s.file
	default:
		return fmt.Sprintf("%s:%d:%d", s.file, s.line, s.col)
	}
}

// Write writes the tree in s to w, each line indented by ident.  Children
// nodes are indented further by a tab.  Typically indent is "" at the top
// level.  Write is intended to display the contents of Statement, but
// not necessarily reproduce the input of Statement.
func (s *Statement) Write(w io.Writer, indent string) error {
	if s.Keyword == "" {
		// We are just a collection of statements at the top level.
		for _, s := range s.statements {
			if err := s.Write(w, indent); err != nil {
				return err
			}
		}
		return nil
	}

	parts := []string{fmt.Sprintf("%s%s", indent, s.Keyword)}
	if s.HasArgument {
		args := strings.Split(s.Argument, "\n")
		if len(args) == 1 {
			parts = append(parts, fmt.Sprintf(" %q", s.Argument))
		} else {
			parts = append(parts, ` "`, args[0], "\n")
			i := fmt.Sprintf("%*s", len(s.Keyword)+1, "")
			for x, p := range args[1:] {
				s := fmt.Sprintf("%q", p)
				s = s[1 : len(s)-1]
				parts = append(parts, indent, " ", i, s)
				if x == len(args[1:])-1 {
					// last part just needs the closing "
					parts = append(parts, `"`)
				} else {
					parts = append(parts, "\n")
				}
			}
		}
	}

	if len(s.statements) == 0 {
		_, err := fmt.Fprintf(w, "%s;\n", strings.Join(parts, ""))
		return err
	}
	if _, err := fmt.Fprintf(w, "%s {\n", strings.Join(parts, "")); err != nil {
		return err
	}
	for _, s := range s.statements {
		if err := s.Write(w, indent+"\t"); err != nil {
			return err
		}
	}
	if _, err := fmt.Fprintf(w, "%s}\n", indent); err != nil {
		return err
	}
	return nil
}

// ignoreMe is returned to continue processing after an error (the parse will
// fail, but we want to look for more errors).
var ignoreMe = &Statement{}

// Parse parses the input as generic YANG and returns the statements parsed.
// The path parameter should be the source name where input was read from (e.g.,
// the file name the input was read from).  If one more more errors are
// encountered, nil and an error are returned.  The error's text includes all
// errors encountered.
func Parse(input, path string) ([]*Statement, error) {
	var statements []*Statement
	p := &parser{
		lex:      newLexer(input, path),
		errout:   &bytes.Buffer{},
		hitBrace: &Statement{},
	}
	p.lex.errout = p.errout
Loop:
	for {
		switch ns := p.nextStatement(); ns {
		case nil:
			break Loop
		case p.hitBrace:
			fmt.Fprintf(p.errout, "%s:%d:%d: unexpected %c\n", ns.file, ns.line, ns.col, closeBrace)
		default:
			statements = append(statements, ns)
		}
	}

	p.checkStatementDepth()

	if p.errout.Len() == 0 {
		return statements, nil
	}
	return nil, errors.New(strings.TrimSpace(p.errout.String()))

}

// push pushes tokens t back on the input stream so they will be the next
// tokens returned by next.  The tokens list is a LIFO so the final token
// listed to push will be the next token returned.
func (p *parser) push(t ...*token) {
	p.tokens = append(p.tokens, t...)
}

// pop returns the last token pushed, or nil if the token stack is empty.
func (p *parser) pop() *token {
	if n := len(p.tokens); n > 0 {
		n--
		defer func() { p.tokens = p.tokens[:n] }()
		return p.tokens[n]
	}
	return nil
}

// next returns the next token from the lexer.  It also handles string
// concatenation.
func (p *parser) next() *token {
	if t := p.pop(); t != nil {
		return t
	}
	next := func() *token {
		for {
			if t := p.lex.NextToken(); t.Code() != tError {
				return t
			}
		}
	}
	t := next()
	if t.Code() != tString {
		return t
	}
	// Handle `"string" + "string"`
	for {
		nt := next()
		switch nt.Code() {
		case tEOF:
			return t
		case tIdentifier:
			if nt.Text != "+" {
				p.push(nt)
				return t
			}
		default:
			p.push(nt)
			return t
		}
		// We found a +, now look for a following string
		st := next()
		switch st.Code() {
		case tEOF:
			p.push(nt)
			return t
		case tString:
			// concatenate the text and drop the nt and st tokens
			// try again
			t.Text += st.Text
		default:
			p.push(st, nt)
			return t
		}

	}
}

// nextStatement returns the next statement in the input, which may in turn
// recurse to read sub statements.
func (p *parser) nextStatement() *Statement {
	t := p.next()
	switch t.Code() {
	case tEOF:
		return nil
	case closeBrace:
		p.statementDepth -= 1
		p.hitBrace.file = t.File
		p.hitBrace.line = t.Line
		p.hitBrace.col = t.Col
		return p.hitBrace
	case tIdentifier:
	default:
		fmt.Fprintf(p.errout, "%v: not an identifier\n", t)
		return ignoreMe
	}

	s := &Statement{
		Keyword: t.Text,
		file:    t.File,
		line:    t.Line,
		col:     t.Col,
	}

	// The keyword "pattern" must be treated special.  When
	// parsing the argument for "pattern", escape sequences
	// must be expanded differently.
	p.lex.inPattern = t.Text == "pattern"
	t = p.next()
	p.lex.inPattern = false
	switch t.Code() {
	case tString, tIdentifier:
		s.HasArgument = true
		s.Argument = t.Text
		t = p.next()
	}
	switch t.Code() {
	case tEOF:
		fmt.Fprintf(p.errout, "%s: unexpected EOF\n", s.file)
		return nil
	case ';':
		return s
	case openBrace:
		p.statementDepth += 1
		for {
			switch ns := p.nextStatement(); ns {
			case nil:
				return nil
			case p.hitBrace:
				return s
			default:
				s.statements = append(s.statements, ns)
			}
		}
	default:
		fmt.Fprintf(p.errout, "%v: syntax error\n", t)
		return ignoreMe
	}
}

// Checks that we have a statement depth of 0. It's an error to exit
// the parser with a depth of > 0, it means we are missing closing
// braces. Note: the parser will error out for the case where we
// start with an unmatched close brace, eg. depth < 0
//
// This test is only done if there are no other errors as
// we may exit early due to those errors -- and therefore there *might*
// not really be a mismatched brace issue.
func (p *parser) checkStatementDepth() {
	if p.errout.Len() > 0 || p.statementDepth < 1 {
		return
	}

	plural := ""
	if p.statementDepth > 1 {
		plural = "s"
	}
	fmt.Fprintf(p.errout, "%s:%d:%d: missing %d closing brace%s\n",
		p.lex.file, p.lex.line, p.lex.col, p.statementDepth, plural)
}
<|endoftext|>


<commit_before>package main

import (
	"errors"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/deiwin/luncher-api/db"
	"github.com/deiwin/luncher-api/db/model"
	"github.com/deiwin/luncher-api/lunchman/interact"
	"gopkg.in/alecthomas/kingpin.v1"
	"gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"
)

var (
	lunchman      = kingpin.New("lunchman", "An administrative tool to manage your luncher instance")
	add           = lunchman.Command("add", "Add a new value to the DB")
	addRegion     = add.Command("region", "Add a region")
	addRestaurant = add.Command("restaurant", "Add a restarant")

	checkNotEmpty = func(i string) error {
		if i == "" {
			return errors.New("Can't be empty!")
		}
		return nil
	}
	checkSingleArg = func(i string) error {
		if strings.Contains(i, " ") {
			return errors.New("Expecting a single argument")
		}
		return nil
	}
	checkValidLocation = func(i string) error {
		if i == "Local" {
			return errors.New("Can't use region 'Local'!")
		} else if _, err := time.LoadLocation(i); err != nil {
			return err
		}
		return nil
	}
)

func main() {
	dbConfig := db.NewConfig()
	dbClient := db.NewClient(dbConfig)
	err := dbClient.Connect()
	if err != nil {
		panic(err)
	}
	defer dbClient.Disconnect()

	actor := interact.NewActor(os.Stdin, os.Stdout)

	switch kingpin.MustParse(lunchman.Parse(os.Args[1:])) {
	case addRegion.FullCommand():
		regionsCollection := db.NewRegions(dbClient)
		checkUnique := getRegionUniquenessCheck(regionsCollection)

		name := getInputOrExit(actor, "Please enter a name for the new region", checkNotEmpty, checkSingleArg, checkUnique)
		location := getInputOrExit(actor, "Please enter the region's location (IANA tz)", checkNotEmpty, checkSingleArg, checkValidLocation)

		insertRegion(actor, regionsCollection, name, location)

		fmt.Println("Region successfully added!")
	case addRestaurant.FullCommand():
		restaurantsCollection := db.NewRestaurants(dbClient)
		usersCollection := db.NewUsers(dbClient)
		regionsCollection := db.NewRegions(dbClient)
		checkUnique := getRestaurantUniquenessCheck(restaurantsCollection)
		checkExists := getRegionExistanceCheck(regionsCollection)

		name := getInputOrExit(actor, "Please enter a name for the new restaurant", checkNotEmpty, checkUnique)
		address := getInputOrExit(actor, "Please enter the restaurant's address", checkNotEmpty)
		region := getInputOrExit(actor, "Please enter the region you want to register the restaurant into", checkNotEmpty, checkExists)
		fbUserID := getInputOrExit(actor, "Please enter the restaurant administrator's Facebook user ID", checkNotEmpty)
		fbPageID := getInputOrExit(actor, "Please enter the restaurant's Facebook page ID", checkNotEmpty)

		restaurantID := insertRestaurantAndGetID(actor, restaurantsCollection, name, address, region)
		insertUser(actor, usersCollection, restaurantID, fbPageID, fbUserID)

		fmt.Println("Restaurant (and user) successfully added!")
	}
}

func insertRegion(actor interact.Actor, regionsCollection db.Regions, name, location string) {
	region := &model.Region{
		Name:     name,
		Location: location,
	}
	confirmDBInsertion(actor, region)
	if err := regionsCollection.Insert(region); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func insertRestaurantAndGetID(actor interact.Actor, restaurantsCollection db.Restaurants, name, address, region string) bson.ObjectId {
	restaurant := &model.Restaurant{
		Name:    name,
		Address: address,
		Region:  region,
	}
	confirmDBInsertion(actor, restaurant)
	insertedRestaurants, err := restaurantsCollection.Insert(restaurant)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	restaurantID := insertedRestaurants[0].ID
	return restaurantID
}

func insertUser(actor interact.Actor, usersCollection db.Users, restaurantID bson.ObjectId, fbUserID, fbPageID string) {
	user := &model.User{
		RestaurantID:   restaurantID,
		FacebookUserID: fbUserID,
		FacebookPageID: fbPageID,
	}
	err := usersCollection.Insert(user)
	if err != nil {
		fmt.Println(err)
		fmt.Println("Failed to enter the new user to the DB while the restaurant was already inserted. Make sure to check the DB for consistency!")
		os.Exit(1)
	}
}

func confirmDBInsertion(actor interact.Actor, o interface{}) {
	confirmationMessage := fmt.Sprintf("Going to enter the following into the DB:\n%+v\nAre you sure you want to continue?", o)
	confirmed, err := actor.Confirm(confirmationMessage, interact.ConfirmDefaultToYes)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	} else if !confirmed {
		fmt.Println("Aborted")
		os.Exit(1)
	}
}

func getInputOrExit(a interact.Actor, message string, checks ...interact.InputCheck) string {
	input, err := a.GetInputAndRetry(message, checks...)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	return input
}

func getRegionExistanceCheck(c db.Regions) interact.InputCheck {
	return func(i string) error {
		if _, err := c.Get(i); err != nil {
			return err
		}
		return nil
	}
}

func getRegionUniquenessCheck(c db.Regions) interact.InputCheck {
	return func(i string) error {
		if _, err := c.Get(i); err != mgo.ErrNotFound {
			if err != nil {
				return err
			}
			return errors.New("A region with the same name already exists!")
		}
		return nil
	}
}

func getRestaurantUniquenessCheck(c db.Restaurants) interact.InputCheck {
	return func(i string) error {
		if exists, err := c.Exists(i); err != nil {
			return err
		} else if exists {
			return errors.New("A restaurant with the same name already exists!")
		}
		return nil
	}
}
<commit_msg>fix lunchman mixing up user and page IDs<commit_after>package main

import (
	"errors"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/deiwin/luncher-api/db"
	"github.com/deiwin/luncher-api/db/model"
	"github.com/deiwin/luncher-api/lunchman/interact"
	"gopkg.in/alecthomas/kingpin.v1"
	"gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"
)

var (
	lunchman      = kingpin.New("lunchman", "An administrative tool to manage your luncher instance")
	add           = lunchman.Command("add", "Add a new value to the DB")
	addRegion     = add.Command("region", "Add a region")
	addRestaurant = add.Command("restaurant", "Add a restarant")

	checkNotEmpty = func(i string) error {
		if i == "" {
			return errors.New("Can't be empty!")
		}
		return nil
	}
	checkSingleArg = func(i string) error {
		if strings.Contains(i, " ") {
			return errors.New("Expecting a single argument")
		}
		return nil
	}
	checkValidLocation = func(i string) error {
		if i == "Local" {
			return errors.New("Can't use region 'Local'!")
		} else if _, err := time.LoadLocation(i); err != nil {
			return err
		}
		return nil
	}
)

func main() {
	dbConfig := db.NewConfig()
	dbClient := db.NewClient(dbConfig)
	err := dbClient.Connect()
	if err != nil {
		panic(err)
	}
	defer dbClient.Disconnect()

	actor := interact.NewActor(os.Stdin, os.Stdout)

	switch kingpin.MustParse(lunchman.Parse(os.Args[1:])) {
	case addRegion.FullCommand():
		regionsCollection := db.NewRegions(dbClient)
		checkUnique := getRegionUniquenessCheck(regionsCollection)

		name := getInputOrExit(actor, "Please enter a name for the new region", checkNotEmpty, checkSingleArg, checkUnique)
		location := getInputOrExit(actor, "Please enter the region's location (IANA tz)", checkNotEmpty, checkSingleArg, checkValidLocation)

		insertRegion(actor, regionsCollection, name, location)

		fmt.Println("Region successfully added!")
	case addRestaurant.FullCommand():
		restaurantsCollection := db.NewRestaurants(dbClient)
		usersCollection := db.NewUsers(dbClient)
		regionsCollection := db.NewRegions(dbClient)
		checkUnique := getRestaurantUniquenessCheck(restaurantsCollection)
		checkExists := getRegionExistanceCheck(regionsCollection)

		name := getInputOrExit(actor, "Please enter a name for the new restaurant", checkNotEmpty, checkUnique)
		address := getInputOrExit(actor, "Please enter the restaurant's address", checkNotEmpty)
		region := getInputOrExit(actor, "Please enter the region you want to register the restaurant into", checkNotEmpty, checkExists)
		fbUserID := getInputOrExit(actor, "Please enter the restaurant administrator's Facebook user ID", checkNotEmpty)
		fbPageID := getInputOrExit(actor, "Please enter the restaurant's Facebook page ID", checkNotEmpty)

		restaurantID := insertRestaurantAndGetID(actor, restaurantsCollection, name, address, region)
		insertUser(actor, usersCollection, restaurantID, fbUserID, fbPageID)

		fmt.Println("Restaurant (and user) successfully added!")
	}
}

func insertRegion(actor interact.Actor, regionsCollection db.Regions, name, location string) {
	region := &model.Region{
		Name:     name,
		Location: location,
	}
	confirmDBInsertion(actor, region)
	if err := regionsCollection.Insert(region); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func insertRestaurantAndGetID(actor interact.Actor, restaurantsCollection db.Restaurants, name, address, region string) bson.ObjectId {
	restaurant := &model.Restaurant{
		Name:    name,
		Address: address,
		Region:  region,
	}
	confirmDBInsertion(actor, restaurant)
	insertedRestaurants, err := restaurantsCollection.Insert(restaurant)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	restaurantID := insertedRestaurants[0].ID
	return restaurantID
}

func insertUser(actor interact.Actor, usersCollection db.Users, restaurantID bson.ObjectId, fbUserID, fbPageID string) {
	user := &model.User{
		RestaurantID:   restaurantID,
		FacebookUserID: fbUserID,
		FacebookPageID: fbPageID,
	}
	err := usersCollection.Insert(user)
	if err != nil {
		fmt.Println(err)
		fmt.Println("Failed to enter the new user to the DB while the restaurant was already inserted. Make sure to check the DB for consistency!")
		os.Exit(1)
	}
}

func confirmDBInsertion(actor interact.Actor, o interface{}) {
	confirmationMessage := fmt.Sprintf("Going to enter the following into the DB:\n%+v\nAre you sure you want to continue?", o)
	confirmed, err := actor.Confirm(confirmationMessage, interact.ConfirmDefaultToYes)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	} else if !confirmed {
		fmt.Println("Aborted")
		os.Exit(1)
	}
}

func getInputOrExit(a interact.Actor, message string, checks ...interact.InputCheck) string {
	input, err := a.GetInputAndRetry(message, checks...)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	return input
}

func getRegionExistanceCheck(c db.Regions) interact.InputCheck {
	return func(i string) error {
		if _, err := c.Get(i); err != nil {
			return err
		}
		return nil
	}
}

func getRegionUniquenessCheck(c db.Regions) interact.InputCheck {
	return func(i string) error {
		if _, err := c.Get(i); err != mgo.ErrNotFound {
			if err != nil {
				return err
			}
			return errors.New("A region with the same name already exists!")
		}
		return nil
	}
}

func getRestaurantUniquenessCheck(c db.Restaurants) interact.InputCheck {
	return func(i string) error {
		if exists, err := c.Exists(i); err != nil {
			return err
		} else if exists {
			return errors.New("A restaurant with the same name already exists!")
		}
		return nil
	}
}
<|endoftext|>


<commit_before>package events

import (
	"context"
	"encoding/json"
	"fmt"
	"sync"
	"time"

	"github.com/pborman/uuid"

	"github.com/gorilla/websocket"
	"github.com/lxc/lxd/shared"
	"github.com/lxc/lxd/shared/api"
	log "github.com/lxc/lxd/shared/log15"
	"github.com/lxc/lxd/shared/logger"
)

// Server represents an instance of an event server.
type Server struct {
	debug   bool
	verbose bool

	listeners map[string]*Listener
	lock      sync.Mutex
}

// NewServer returns a new event server.
func NewServer(debug bool, verbose bool) *Server {
	server := &Server{
		debug:     debug,
		verbose:   verbose,
		listeners: map[string]*Listener{},
	}

	return server
}

// AddListener creates and returns a new event listener.
func (s *Server) AddListener(group string, connection *websocket.Conn, messageTypes []string, location string, noForward bool) (*Listener, error) {
	ctx, ctxCancel := context.WithCancel(context.Background())

	listener := &Listener{
		Conn: connection,

		group:        group,
		messageTypes: messageTypes,
		location:     location,
		noForward:    noForward,
		ctx:          ctx,
		ctxCancel:    ctxCancel,
		id:           uuid.New(),
	}

	s.lock.Lock()
	defer s.lock.Unlock()

	if s.listeners[listener.id] != nil {
		return nil, fmt.Errorf("A listener with id '%s' already exists", listener.id)
	}

	s.listeners[listener.id] = listener

	go listener.heartbeat()

	return listener, nil
}

// SendLifecycle broadcasts a lifecycle event.
func (s *Server) SendLifecycle(group string, event api.EventLifecycle) {
	s.Send(group, "lifecycle", event)
}

// Send broadcasts a custom event.
func (s *Server) Send(group, eventType string, eventMessage interface{}) error {
	encodedMessage, err := json.Marshal(eventMessage)
	if err != nil {
		return err
	}
	event := api.Event{
		Type:      eventType,
		Timestamp: time.Now(),
		Metadata:  encodedMessage,
	}

	return s.broadcast(group, event, false)
}

// Forward to the local events dispatcher an event received from another node.
func (s *Server) Forward(id int64, event api.Event) {
	if event.Type == "logging" {
		// Parse the message
		logEntry := api.EventLogging{}
		err := json.Unmarshal(event.Metadata, &logEntry)
		if err != nil {
			return
		}

		if !s.debug && logEntry.Level == "dbug" {
			return
		}

		if !s.debug && !s.verbose && logEntry.Level == "info" {
			return
		}
	}

	err := s.broadcast("", event, true)
	if err != nil {
		logger.Warnf("Failed to forward event from node %d: %v", id, err)
	}
}

func (s *Server) broadcast(group string, event api.Event, isForward bool) error {
	s.lock.Lock()
	listeners := s.listeners
	for _, listener := range listeners {
		if group != "" && listener.group != "*" && group != listener.group {
			continue
		}

		if isForward && listener.noForward {
			continue
		}

		if !shared.StringInSlice(event.Type, listener.messageTypes) {
			continue
		}

		go func(listener *Listener, event api.Event) {
			// Check that the listener still exists
			if listener == nil {
				return
			}

			// Make sure we're not done already
			if listener.IsClosed() {
				return
			}

			// Set the Location to the expected serverName
			if event.Location == "" {
				eventCopy := api.Event{}
				err := shared.DeepCopy(&event, &eventCopy)
				if err != nil {
					return
				}
				eventCopy.Location = listener.location

				event = eventCopy
			}

			listener.SetWriteDeadline(time.Now().Add(5 * time.Second))
			err := listener.WriteJSON(event)
			if err != nil {
				// Remove the listener from the list
				s.lock.Lock()
				delete(s.listeners, listener.id)
				s.lock.Unlock()

				listener.Close()
			}
		}(listener, event)
	}
	s.lock.Unlock()

	return nil
}

// Listener describes an event listener.
type Listener struct {
	*websocket.Conn

	group        string
	messageTypes []string
	ctx          context.Context
	ctxCancel    func()
	id           string
	lock         sync.Mutex
	location     string
	lastPong     time.Time

	// If true, this listener won't get events forwarded from other
	// nodes. It only used by listeners created internally by LXD nodes
	// connecting to other LXD nodes to get their local events only.
	noForward bool
}

func (e *Listener) heartbeat() {
	defer e.Close()

	pingInterval := time.Second * 5
	e.lastPong = time.Now() // To allow initial heartbeat ping to be sent.

	e.SetPongHandler(func(msg string) error {
		e.lastPong = time.Now()
		return nil
	})

	// Run a blocking reader to detect if the remote side is closed.
	// We don't expect to get anything from the remote side, so this should remain blocked until disconnected.
	go func() {
		e.Conn.NextReader()
		e.Close()
	}()

	for {
		if e.IsClosed() {
			return
		}

		if e.lastPong.Add(pingInterval * 2).Before(time.Now()) {
			logger.Warn("Hearbeat for event listener timed out", log.Ctx{"listener": e.ID()})
			return
		}

		e.lock.Lock()
		err := e.WriteControl(websocket.PingMessage, []byte("keepalive"), time.Now().Add(5*time.Second))
		if err != nil {
			e.lock.Unlock()
			return
		}
		e.lock.Unlock()

		select {
		case <-time.After(pingInterval):
		case <-e.ctx.Done():
			return
		}
	}
}

// MessageTypes returns a list of message types the listener will be notified of.
func (e *Listener) MessageTypes() []string {
	return e.messageTypes
}

// IsClosed returns true if the listener is closed.
func (e *Listener) IsClosed() bool {
	return e.ctx.Err() != nil
}

// ID returns the listener ID.
func (e *Listener) ID() string {
	return e.id
}

// Wait waits for a message on its active channel or the context is cancelled, then returns.
func (e *Listener) Wait(ctx context.Context) {
	select {
	case <-ctx.Done():
	case <-e.ctx.Done():
	}
}

// Close Disconnects the listener.
func (e *Listener) Close() {
	e.lock.Lock()
	defer e.lock.Unlock()

	if e.IsClosed() {
		return
	}

	logger.Debug("Disconnected event listener", log.Ctx{"listener": e.id})

	e.Conn.Close()
	e.ctxCancel()
}

// WriteJSON message to the connection.
func (e *Listener) WriteJSON(v interface{}) error {
	e.lock.Lock()
	defer e.lock.Unlock()

	return e.Conn.WriteJSON(v)
}

// WriteMessage to the connection.
func (e *Listener) WriteMessage(messageType int, data []byte) error {
	e.lock.Lock()
	defer e.lock.Unlock()

	return e.Conn.WriteMessage(messageType, data)
}
<commit_msg>lxd/events/events: Switch events heartbeat to counter rather than using absolute deadline times<commit_after>package events

import (
	"context"
	"encoding/json"
	"fmt"
	"sync"
	"time"

	"github.com/pborman/uuid"

	"github.com/gorilla/websocket"
	"github.com/lxc/lxd/shared"
	"github.com/lxc/lxd/shared/api"
	log "github.com/lxc/lxd/shared/log15"
	"github.com/lxc/lxd/shared/logger"
)

// Server represents an instance of an event server.
type Server struct {
	debug   bool
	verbose bool

	listeners map[string]*Listener
	lock      sync.Mutex
}

// NewServer returns a new event server.
func NewServer(debug bool, verbose bool) *Server {
	server := &Server{
		debug:     debug,
		verbose:   verbose,
		listeners: map[string]*Listener{},
	}

	return server
}

// AddListener creates and returns a new event listener.
func (s *Server) AddListener(group string, connection *websocket.Conn, messageTypes []string, location string, noForward bool) (*Listener, error) {
	ctx, ctxCancel := context.WithCancel(context.Background())

	listener := &Listener{
		Conn: connection,

		group:        group,
		messageTypes: messageTypes,
		location:     location,
		noForward:    noForward,
		ctx:          ctx,
		ctxCancel:    ctxCancel,
		id:           uuid.New(),
	}

	s.lock.Lock()
	defer s.lock.Unlock()

	if s.listeners[listener.id] != nil {
		return nil, fmt.Errorf("A listener with id '%s' already exists", listener.id)
	}

	s.listeners[listener.id] = listener

	go listener.heartbeat()

	return listener, nil
}

// SendLifecycle broadcasts a lifecycle event.
func (s *Server) SendLifecycle(group string, event api.EventLifecycle) {
	s.Send(group, "lifecycle", event)
}

// Send broadcasts a custom event.
func (s *Server) Send(group, eventType string, eventMessage interface{}) error {
	encodedMessage, err := json.Marshal(eventMessage)
	if err != nil {
		return err
	}
	event := api.Event{
		Type:      eventType,
		Timestamp: time.Now(),
		Metadata:  encodedMessage,
	}

	return s.broadcast(group, event, false)
}

// Forward to the local events dispatcher an event received from another node.
func (s *Server) Forward(id int64, event api.Event) {
	if event.Type == "logging" {
		// Parse the message
		logEntry := api.EventLogging{}
		err := json.Unmarshal(event.Metadata, &logEntry)
		if err != nil {
			return
		}

		if !s.debug && logEntry.Level == "dbug" {
			return
		}

		if !s.debug && !s.verbose && logEntry.Level == "info" {
			return
		}
	}

	err := s.broadcast("", event, true)
	if err != nil {
		logger.Warnf("Failed to forward event from node %d: %v", id, err)
	}
}

func (s *Server) broadcast(group string, event api.Event, isForward bool) error {
	s.lock.Lock()
	listeners := s.listeners
	for _, listener := range listeners {
		if group != "" && listener.group != "*" && group != listener.group {
			continue
		}

		if isForward && listener.noForward {
			continue
		}

		if !shared.StringInSlice(event.Type, listener.messageTypes) {
			continue
		}

		go func(listener *Listener, event api.Event) {
			// Check that the listener still exists
			if listener == nil {
				return
			}

			// Make sure we're not done already
			if listener.IsClosed() {
				return
			}

			// Set the Location to the expected serverName
			if event.Location == "" {
				eventCopy := api.Event{}
				err := shared.DeepCopy(&event, &eventCopy)
				if err != nil {
					return
				}
				eventCopy.Location = listener.location

				event = eventCopy
			}

			listener.SetWriteDeadline(time.Now().Add(5 * time.Second))
			err := listener.WriteJSON(event)
			if err != nil {
				// Remove the listener from the list
				s.lock.Lock()
				delete(s.listeners, listener.id)
				s.lock.Unlock()

				listener.Close()
			}
		}(listener, event)
	}
	s.lock.Unlock()

	return nil
}

// Listener describes an event listener.
type Listener struct {
	*websocket.Conn

	group        string
	messageTypes []string
	ctx          context.Context
	ctxCancel    func()
	id           string
	lock         sync.Mutex
	location     string
	pongsPending uint

	// If true, this listener won't get events forwarded from other
	// nodes. It only used by listeners created internally by LXD nodes
	// connecting to other LXD nodes to get their local events only.
	noForward bool
}

func (e *Listener) heartbeat() {
	defer e.Close()

	pingInterval := time.Second * 5
	e.pongsPending = 0

	e.SetPongHandler(func(msg string) error {
		e.lock.Lock()
		e.pongsPending = 0
		e.lock.Unlock()
		return nil
	})

	// Run a blocking reader to detect if the remote side is closed.
	// We don't expect to get anything from the remote side, so this should remain blocked until disconnected.
	go func() {
		e.Conn.NextReader()
		e.Close()
	}()

	for {
		if e.IsClosed() {
			return
		}

		e.lock.Lock()
		if e.pongsPending > 2 {
			e.lock.Unlock()
			logger.Warn("Hearbeat for event listener timed out", log.Ctx{"listener": e.ID()})
			return
		}
		err := e.WriteControl(websocket.PingMessage, []byte("keepalive"), time.Now().Add(5*time.Second))
		if err != nil {
			e.lock.Unlock()
			return
		}

		e.pongsPending++
		e.lock.Unlock()

		select {
		case <-time.After(pingInterval):
		case <-e.ctx.Done():
			return
		}
	}
}

// MessageTypes returns a list of message types the listener will be notified of.
func (e *Listener) MessageTypes() []string {
	return e.messageTypes
}

// IsClosed returns true if the listener is closed.
func (e *Listener) IsClosed() bool {
	return e.ctx.Err() != nil
}

// ID returns the listener ID.
func (e *Listener) ID() string {
	return e.id
}

// Wait waits for a message on its active channel or the context is cancelled, then returns.
func (e *Listener) Wait(ctx context.Context) {
	select {
	case <-ctx.Done():
	case <-e.ctx.Done():
	}
}

// Close Disconnects the listener.
func (e *Listener) Close() {
	e.lock.Lock()
	defer e.lock.Unlock()

	if e.IsClosed() {
		return
	}

	logger.Debug("Disconnected event listener", log.Ctx{"listener": e.id})

	e.Conn.Close()
	e.ctxCancel()
}

// WriteJSON message to the connection.
func (e *Listener) WriteJSON(v interface{}) error {
	e.lock.Lock()
	defer e.lock.Unlock()

	return e.Conn.WriteJSON(v)
}

// WriteMessage to the connection.
func (e *Listener) WriteMessage(messageType int, data []byte) error {
	e.lock.Lock()
	defer e.lock.Unlock()

	return e.Conn.WriteMessage(messageType, data)
}
<|endoftext|>


<commit_before>package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"sync"

	"github.com/gorilla/mux"
	lxd "github.com/lxc/lxd/client"
	"github.com/lxc/lxd/lxd/cluster"
	"github.com/lxc/lxd/lxd/db"
	"github.com/lxc/lxd/lxd/util"
	"github.com/lxc/lxd/shared/api"
	"github.com/lxc/lxd/shared/version"
)

// Lock to prevent concurent storage pools creation
var storagePoolCreateLock sync.Mutex

// /1.0/storage-pools
// List all storage pools.
func storagePoolsGet(d *Daemon, r *http.Request) Response {
	recursionStr := r.FormValue("recursion")
	recursion, err := strconv.Atoi(recursionStr)
	if err != nil {
		recursion = 0
	}

	pools, err := d.cluster.StoragePools()
	if err != nil && err != db.NoSuchObjectError {
		return SmartError(err)
	}

	resultString := []string{}
	resultMap := []api.StoragePool{}
	for _, pool := range pools {
		if recursion == 0 {
			resultString = append(resultString, fmt.Sprintf("/%s/storage-pools/%s", version.APIVersion, pool))
		} else {
			plID, pl, err := d.cluster.StoragePoolGet(pool)
			if err != nil {
				continue
			}

			// Get all users of the storage pool.
			poolUsedBy, err := storagePoolUsedByGet(d.State(), plID, pool)
			if err != nil {
				return SmartError(err)
			}
			pl.UsedBy = poolUsedBy

			resultMap = append(resultMap, *pl)
		}
	}

	if recursion == 0 {
		return SyncResponse(true, resultString)
	}

	return SyncResponse(true, resultMap)
}

// /1.0/storage-pools
// Create a storage pool.
func storagePoolsPost(d *Daemon, r *http.Request) Response {
	storagePoolCreateLock.Lock()
	defer storagePoolCreateLock.Unlock()

	req := api.StoragePoolsPost{}

	// Parse the request.
	err := json.NewDecoder(r.Body).Decode(&req)
	if err != nil {
		return BadRequest(err)
	}

	// Sanity checks.
	if req.Name == "" {
		return BadRequest(fmt.Errorf("No name provided"))
	}

	if strings.Contains(req.Name, "/") {
		return BadRequest(fmt.Errorf("Storage pool names may not contain slashes"))
	}

	if req.Driver == "" {
		return BadRequest(fmt.Errorf("No driver provided"))
	}

	url := fmt.Sprintf("/%s/storage-pools/%s", version.APIVersion, req.Name)
	response := SyncResponseLocation(true, nil, url)

	if isClusterNotification(r) {
		// This is an internal request which triggers the actual
		// creation of the pool across all nodes, after they have been
		// previously defined.
		err = doStoragePoolCreateInternal(
			d.State(), req.Name, req.Description, req.Driver, req.Config)
		if err != nil {
			return SmartError(err)
		}
		return response
	}

	targetNode := r.FormValue("targetNode")
	if targetNode == "" {
		count, err := cluster.Count(d.State())
		if err != nil {
			return SmartError(err)
		}

		if count == 1 {
			// No targetNode was specified and we're either a single-node
			// cluster or not clustered at all, so create the storage
			// pool immediately.
			err = storagePoolCreateInternal(
				d.State(), req.Name, req.Description, req.Driver, req.Config)
		} else {
			// No targetNode was specified and we're clustered, so finalize the
			// config in the db and actually create the pool on all nodes.
			err = storagePoolsPostCluster(d, req)
		}
		if err != nil {
			return InternalError(err)
		}
		return response

	}

	// A targetNode was specified, let's just define the node's storage
	// without actually creating it. The only legal key value for the
	// storage config is 'source'.
	for key := range req.Config {
		if key != "source" {
			return SmartError(fmt.Errorf("Invalid config key '%s'", key))
		}
	}
	err = d.cluster.Transaction(func(tx *db.ClusterTx) error {
		return tx.StoragePoolCreatePending(targetNode, req.Name, req.Driver, req.Config)
	})
	if err != nil {
		return SmartError(err)
	}

	return response
}

func storagePoolsPostCluster(d *Daemon, req api.StoragePoolsPost) error {
	// Check that no 'source' config key has been defined, since
	// that's node-specific.
	for key := range req.Config {
		if key == "source" {
			return fmt.Errorf("Config key 'source' is node-specific")
		}
	}

	// Check that the pool is properly defined, fetch the node-specific
	// configs and insert the global config.
	var configs map[string]map[string]string
	var nodeName string
	err := d.cluster.Transaction(func(tx *db.ClusterTx) error {
		// Check that the pool was defined at all.
		poolID, err := tx.StoragePoolID(req.Name)
		if err != nil {
			return err
		}

		// Fetch the node-specific configs.
		configs, err = tx.StoragePoolNodeConfigs(poolID)
		if err != nil {
			return err
		}

		// Take note of the name of this node
		nodeName, err = tx.NodeName()
		if err != nil {
			return err
		}

		// Insert the global config keys.
		return tx.StoragePoolConfigAdd(poolID, 0, req.Config)
	})
	if err != nil {
		return err
	}

	// Create the pool on this node.
	nodeReq := req
	for key, value := range configs[nodeName] {
		nodeReq.Config[key] = value
	}
	err = doStoragePoolCreateInternal(
		d.State(), req.Name, req.Description, req.Driver, req.Config)
	if err != nil {
		return err
	}

	// Notify all other nodes to create the pool.
	notifier, err := cluster.NewNotifier(d.State(), d.endpoints.NetworkCert(), cluster.NotifyAll)
	if err != nil {
		return err
	}
	notifyErr := notifier(func(client lxd.ContainerServer) error {
		_, _, err := client.GetServer()
		if err != nil {
			return err
		}
		nodeReq := req
		for key, value := range configs[client.ClusterNodeName()] {
			nodeReq.Config[key] = value
		}
		return client.CreateStoragePool(nodeReq)
	})

	errored := notifyErr != nil

	// Finally update the storage pool state.
	err = d.cluster.Transaction(func(tx *db.ClusterTx) error {
		if errored {
			return tx.StoragePoolErrored(req.Name)
		}
		return tx.StoragePoolCreated(req.Name)
	})
	if err != nil {
		return err
	}

	return notifyErr
}

var storagePoolsCmd = Command{name: "storage-pools", get: storagePoolsGet, post: storagePoolsPost}

// /1.0/storage-pools/{name}
// Get a single storage pool.
func storagePoolGet(d *Daemon, r *http.Request) Response {
	poolName := mux.Vars(r)["name"]

	// Get the existing storage pool.
	poolID, pool, err := d.cluster.StoragePoolGet(poolName)
	if err != nil {
		return SmartError(err)
	}

	// Get all users of the storage pool.
	poolUsedBy, err := storagePoolUsedByGet(d.State(), poolID, poolName)
	if err != nil && err != db.NoSuchObjectError {
		return SmartError(err)
	}
	pool.UsedBy = poolUsedBy

	targetNode := r.FormValue("targetNode")

	// If no target node is specified and the client is clustered, we omit
	// the node-specific fields, namely "source"
	clustered, err := cluster.Enabled(d.db)
	if err != nil {
		return SmartError(err)
	}
	if targetNode == "" && clustered {
		delete(pool.Config, "source")
	}

	// If a target was specified, forward the request to the relevant node.
	if targetNode != "" {
		address, err := cluster.ResolveTarget(d.cluster, targetNode)
		if err != nil {
			return SmartError(err)
		}
		if address != "" {
			cert := d.endpoints.NetworkCert()
			client, err := cluster.Connect(address, cert, true)
			if err != nil {
				return SmartError(err)
			}
			client = client.ClusterTargetNode(targetNode)
			pool, _, err = client.GetStoragePool(poolName)
			if err != nil {
				return SmartError(err)
			}
		}
	}

	etag := []interface{}{pool.Name, pool.Driver, pool.Config}

	return SyncResponseETag(true, &pool, etag)
}

// /1.0/storage-pools/{name}
// Replace pool properties.
func storagePoolPut(d *Daemon, r *http.Request) Response {
	poolName := mux.Vars(r)["name"]

	// Get the existing storage pool.
	_, dbInfo, err := d.cluster.StoragePoolGet(poolName)
	if err != nil {
		return SmartError(err)
	}

	// Validate the ETag
	etag := []interface{}{dbInfo.Name, dbInfo.Driver, dbInfo.Config}

	err = util.EtagCheck(r, etag)
	if err != nil {
		return PreconditionFailed(err)
	}

	req := api.StoragePoolPut{}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		return BadRequest(err)
	}

	// Validate the configuration
	err = storagePoolValidateConfig(poolName, dbInfo.Driver, req.Config, dbInfo.Config)
	if err != nil {
		return BadRequest(err)
	}

	err = storagePoolUpdate(d.State(), poolName, req.Description, req.Config)
	if err != nil {
		return InternalError(err)
	}

	return EmptySyncResponse
}

// /1.0/storage-pools/{name}
// Change pool properties.
func storagePoolPatch(d *Daemon, r *http.Request) Response {
	poolName := mux.Vars(r)["name"]

	// Get the existing network
	_, dbInfo, err := d.cluster.StoragePoolGet(poolName)
	if dbInfo != nil {
		return SmartError(err)
	}

	// Validate the ETag
	etag := []interface{}{dbInfo.Name, dbInfo.Driver, dbInfo.Config}

	err = util.EtagCheck(r, etag)
	if err != nil {
		return PreconditionFailed(err)
	}

	req := api.StoragePoolPut{}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		return BadRequest(err)
	}

	// Config stacking
	if req.Config == nil {
		req.Config = map[string]string{}
	}

	for k, v := range dbInfo.Config {
		_, ok := req.Config[k]
		if !ok {
			req.Config[k] = v
		}
	}

	// Validate the configuration
	err = storagePoolValidateConfig(poolName, dbInfo.Driver, req.Config, dbInfo.Config)
	if err != nil {
		return BadRequest(err)
	}

	err = storagePoolUpdate(d.State(), poolName, req.Description, req.Config)
	if err != nil {
		return InternalError(err)
	}

	return EmptySyncResponse
}

// /1.0/storage-pools/{name}
// Delete storage pool.
func storagePoolDelete(d *Daemon, r *http.Request) Response {
	poolName := mux.Vars(r)["name"]

	poolID, err := d.cluster.StoragePoolGetID(poolName)
	if err != nil {
		return NotFound
	}

	// Check if the storage pool has any volumes associated with it, if so
	// error out.
	volumeCount, err := d.cluster.StoragePoolVolumesGetNames(poolID)
	if err != nil {
		return InternalError(err)
	}

	if volumeCount > 0 {
		return BadRequest(fmt.Errorf("storage pool \"%s\" has volumes attached to it", poolName))
	}

	// Check if the storage pool is still referenced in any profiles.
	profiles, err := profilesUsingPoolGetNames(d.cluster, poolName)
	if err != nil {
		return SmartError(err)
	}

	if len(profiles) > 0 {
		return BadRequest(fmt.Errorf("Storage pool \"%s\" has profiles using it:\n%s", poolName, strings.Join(profiles, "\n")))
	}

	s, err := storagePoolInit(d.State(), poolName)
	if err != nil {
		return InternalError(err)
	}

	err = s.StoragePoolDelete()
	if err != nil {
		return InternalError(err)
	}

	// If this is a cluster notification, we're done, any database work
	// will be done by the node that is originally serving the request.
	if isClusterNotification(r) {
		return EmptySyncResponse
	}

	// If we are clustered, also notify all other nodes, if any.
	clustered, err := cluster.Enabled(d.db)
	if err != nil {
		return SmartError(err)
	}
	if clustered {
		notifier, err := cluster.NewNotifier(d.State(), d.endpoints.NetworkCert(), cluster.NotifyAll)
		if err != nil {
			return SmartError(err)
		}
		err = notifier(func(client lxd.ContainerServer) error {
			_, _, err := client.GetServer()
			if err != nil {
				return err
			}
			return client.DeleteStoragePool(poolName)
		})
		if err != nil {
			return SmartError(err)
		}
	}

	err = dbStoragePoolDeleteAndUpdateCache(d.cluster, poolName)
	if err != nil {
		return SmartError(err)
	}

	return EmptySyncResponse
}

var storagePoolCmd = Command{name: "storage-pools/{name}", get: storagePoolGet, put: storagePoolPut, patch: storagePoolPatch, delete: storagePoolDelete}
<commit_msg>Handle creating duplicate pending storage pools on the same node<commit_after>package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"sync"

	"github.com/gorilla/mux"
	lxd "github.com/lxc/lxd/client"
	"github.com/lxc/lxd/lxd/cluster"
	"github.com/lxc/lxd/lxd/db"
	"github.com/lxc/lxd/lxd/util"
	"github.com/lxc/lxd/shared/api"
	"github.com/lxc/lxd/shared/version"
)

// Lock to prevent concurent storage pools creation
var storagePoolCreateLock sync.Mutex

// /1.0/storage-pools
// List all storage pools.
func storagePoolsGet(d *Daemon, r *http.Request) Response {
	recursionStr := r.FormValue("recursion")
	recursion, err := strconv.Atoi(recursionStr)
	if err != nil {
		recursion = 0
	}

	pools, err := d.cluster.StoragePools()
	if err != nil && err != db.NoSuchObjectError {
		return SmartError(err)
	}

	resultString := []string{}
	resultMap := []api.StoragePool{}
	for _, pool := range pools {
		if recursion == 0 {
			resultString = append(resultString, fmt.Sprintf("/%s/storage-pools/%s", version.APIVersion, pool))
		} else {
			plID, pl, err := d.cluster.StoragePoolGet(pool)
			if err != nil {
				continue
			}

			// Get all users of the storage pool.
			poolUsedBy, err := storagePoolUsedByGet(d.State(), plID, pool)
			if err != nil {
				return SmartError(err)
			}
			pl.UsedBy = poolUsedBy

			resultMap = append(resultMap, *pl)
		}
	}

	if recursion == 0 {
		return SyncResponse(true, resultString)
	}

	return SyncResponse(true, resultMap)
}

// /1.0/storage-pools
// Create a storage pool.
func storagePoolsPost(d *Daemon, r *http.Request) Response {
	storagePoolCreateLock.Lock()
	defer storagePoolCreateLock.Unlock()

	req := api.StoragePoolsPost{}

	// Parse the request.
	err := json.NewDecoder(r.Body).Decode(&req)
	if err != nil {
		return BadRequest(err)
	}

	// Sanity checks.
	if req.Name == "" {
		return BadRequest(fmt.Errorf("No name provided"))
	}

	if strings.Contains(req.Name, "/") {
		return BadRequest(fmt.Errorf("Storage pool names may not contain slashes"))
	}

	if req.Driver == "" {
		return BadRequest(fmt.Errorf("No driver provided"))
	}

	url := fmt.Sprintf("/%s/storage-pools/%s", version.APIVersion, req.Name)
	response := SyncResponseLocation(true, nil, url)

	if isClusterNotification(r) {
		// This is an internal request which triggers the actual
		// creation of the pool across all nodes, after they have been
		// previously defined.
		err = doStoragePoolCreateInternal(
			d.State(), req.Name, req.Description, req.Driver, req.Config)
		if err != nil {
			return SmartError(err)
		}
		return response
	}

	targetNode := r.FormValue("targetNode")
	if targetNode == "" {
		count, err := cluster.Count(d.State())
		if err != nil {
			return SmartError(err)
		}

		if count == 1 {
			// No targetNode was specified and we're either a single-node
			// cluster or not clustered at all, so create the storage
			// pool immediately.
			err = storagePoolCreateInternal(
				d.State(), req.Name, req.Description, req.Driver, req.Config)
		} else {
			// No targetNode was specified and we're clustered, so finalize the
			// config in the db and actually create the pool on all nodes.
			err = storagePoolsPostCluster(d, req)
		}
		if err != nil {
			return InternalError(err)
		}
		return response

	}

	// A targetNode was specified, let's just define the node's storage
	// without actually creating it. The only legal key value for the
	// storage config is 'source'.
	for key := range req.Config {
		if key != "source" {
			return SmartError(fmt.Errorf("Invalid config key '%s'", key))
		}
	}
	err = d.cluster.Transaction(func(tx *db.ClusterTx) error {
		return tx.StoragePoolCreatePending(targetNode, req.Name, req.Driver, req.Config)
	})
	if err != nil {
		if err == db.DbErrAlreadyDefined {
			return BadRequest(
				fmt.Errorf("The storage pool already defined on node %s", targetNode))
		}
		return SmartError(err)
	}

	return response
}

func storagePoolsPostCluster(d *Daemon, req api.StoragePoolsPost) error {
	// Check that no 'source' config key has been defined, since
	// that's node-specific.
	for key := range req.Config {
		if key == "source" {
			return fmt.Errorf("Config key 'source' is node-specific")
		}
	}

	// Check that the pool is properly defined, fetch the node-specific
	// configs and insert the global config.
	var configs map[string]map[string]string
	var nodeName string
	err := d.cluster.Transaction(func(tx *db.ClusterTx) error {
		// Check that the pool was defined at all.
		poolID, err := tx.StoragePoolID(req.Name)
		if err != nil {
			return err
		}

		// Fetch the node-specific configs.
		configs, err = tx.StoragePoolNodeConfigs(poolID)
		if err != nil {
			return err
		}

		// Take note of the name of this node
		nodeName, err = tx.NodeName()
		if err != nil {
			return err
		}

		// Insert the global config keys.
		return tx.StoragePoolConfigAdd(poolID, 0, req.Config)
	})
	if err != nil {
		return err
	}

	// Create the pool on this node.
	nodeReq := req
	for key, value := range configs[nodeName] {
		nodeReq.Config[key] = value
	}
	err = doStoragePoolCreateInternal(
		d.State(), req.Name, req.Description, req.Driver, req.Config)
	if err != nil {
		return err
	}

	// Notify all other nodes to create the pool.
	notifier, err := cluster.NewNotifier(d.State(), d.endpoints.NetworkCert(), cluster.NotifyAll)
	if err != nil {
		return err
	}
	notifyErr := notifier(func(client lxd.ContainerServer) error {
		_, _, err := client.GetServer()
		if err != nil {
			return err
		}
		nodeReq := req
		for key, value := range configs[client.ClusterNodeName()] {
			nodeReq.Config[key] = value
		}
		return client.CreateStoragePool(nodeReq)
	})

	errored := notifyErr != nil

	// Finally update the storage pool state.
	err = d.cluster.Transaction(func(tx *db.ClusterTx) error {
		if errored {
			return tx.StoragePoolErrored(req.Name)
		}
		return tx.StoragePoolCreated(req.Name)
	})
	if err != nil {
		return err
	}

	return notifyErr
}

var storagePoolsCmd = Command{name: "storage-pools", get: storagePoolsGet, post: storagePoolsPost}

// /1.0/storage-pools/{name}
// Get a single storage pool.
func storagePoolGet(d *Daemon, r *http.Request) Response {
	poolName := mux.Vars(r)["name"]

	// Get the existing storage pool.
	poolID, pool, err := d.cluster.StoragePoolGet(poolName)
	if err != nil {
		return SmartError(err)
	}

	// Get all users of the storage pool.
	poolUsedBy, err := storagePoolUsedByGet(d.State(), poolID, poolName)
	if err != nil && err != db.NoSuchObjectError {
		return SmartError(err)
	}
	pool.UsedBy = poolUsedBy

	targetNode := r.FormValue("targetNode")

	// If no target node is specified and the client is clustered, we omit
	// the node-specific fields, namely "source"
	clustered, err := cluster.Enabled(d.db)
	if err != nil {
		return SmartError(err)
	}
	if targetNode == "" && clustered {
		delete(pool.Config, "source")
	}

	// If a target was specified, forward the request to the relevant node.
	if targetNode != "" {
		address, err := cluster.ResolveTarget(d.cluster, targetNode)
		if err != nil {
			return SmartError(err)
		}
		if address != "" {
			cert := d.endpoints.NetworkCert()
			client, err := cluster.Connect(address, cert, true)
			if err != nil {
				return SmartError(err)
			}
			client = client.ClusterTargetNode(targetNode)
			pool, _, err = client.GetStoragePool(poolName)
			if err != nil {
				return SmartError(err)
			}
		}
	}

	etag := []interface{}{pool.Name, pool.Driver, pool.Config}

	return SyncResponseETag(true, &pool, etag)
}

// /1.0/storage-pools/{name}
// Replace pool properties.
func storagePoolPut(d *Daemon, r *http.Request) Response {
	poolName := mux.Vars(r)["name"]

	// Get the existing storage pool.
	_, dbInfo, err := d.cluster.StoragePoolGet(poolName)
	if err != nil {
		return SmartError(err)
	}

	// Validate the ETag
	etag := []interface{}{dbInfo.Name, dbInfo.Driver, dbInfo.Config}

	err = util.EtagCheck(r, etag)
	if err != nil {
		return PreconditionFailed(err)
	}

	req := api.StoragePoolPut{}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		return BadRequest(err)
	}

	// Validate the configuration
	err = storagePoolValidateConfig(poolName, dbInfo.Driver, req.Config, dbInfo.Config)
	if err != nil {
		return BadRequest(err)
	}

	err = storagePoolUpdate(d.State(), poolName, req.Description, req.Config)
	if err != nil {
		return InternalError(err)
	}

	return EmptySyncResponse
}

// /1.0/storage-pools/{name}
// Change pool properties.
func storagePoolPatch(d *Daemon, r *http.Request) Response {
	poolName := mux.Vars(r)["name"]

	// Get the existing network
	_, dbInfo, err := d.cluster.StoragePoolGet(poolName)
	if dbInfo != nil {
		return SmartError(err)
	}

	// Validate the ETag
	etag := []interface{}{dbInfo.Name, dbInfo.Driver, dbInfo.Config}

	err = util.EtagCheck(r, etag)
	if err != nil {
		return PreconditionFailed(err)
	}

	req := api.StoragePoolPut{}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		return BadRequest(err)
	}

	// Config stacking
	if req.Config == nil {
		req.Config = map[string]string{}
	}

	for k, v := range dbInfo.Config {
		_, ok := req.Config[k]
		if !ok {
			req.Config[k] = v
		}
	}

	// Validate the configuration
	err = storagePoolValidateConfig(poolName, dbInfo.Driver, req.Config, dbInfo.Config)
	if err != nil {
		return BadRequest(err)
	}

	err = storagePoolUpdate(d.State(), poolName, req.Description, req.Config)
	if err != nil {
		return InternalError(err)
	}

	return EmptySyncResponse
}

// /1.0/storage-pools/{name}
// Delete storage pool.
func storagePoolDelete(d *Daemon, r *http.Request) Response {
	poolName := mux.Vars(r)["name"]

	poolID, err := d.cluster.StoragePoolGetID(poolName)
	if err != nil {
		return NotFound
	}

	// Check if the storage pool has any volumes associated with it, if so
	// error out.
	volumeCount, err := d.cluster.StoragePoolVolumesGetNames(poolID)
	if err != nil {
		return InternalError(err)
	}

	if volumeCount > 0 {
		return BadRequest(fmt.Errorf("storage pool \"%s\" has volumes attached to it", poolName))
	}

	// Check if the storage pool is still referenced in any profiles.
	profiles, err := profilesUsingPoolGetNames(d.cluster, poolName)
	if err != nil {
		return SmartError(err)
	}

	if len(profiles) > 0 {
		return BadRequest(fmt.Errorf("Storage pool \"%s\" has profiles using it:\n%s", poolName, strings.Join(profiles, "\n")))
	}

	s, err := storagePoolInit(d.State(), poolName)
	if err != nil {
		return InternalError(err)
	}

	err = s.StoragePoolDelete()
	if err != nil {
		return InternalError(err)
	}

	// If this is a cluster notification, we're done, any database work
	// will be done by the node that is originally serving the request.
	if isClusterNotification(r) {
		return EmptySyncResponse
	}

	// If we are clustered, also notify all other nodes, if any.
	clustered, err := cluster.Enabled(d.db)
	if err != nil {
		return SmartError(err)
	}
	if clustered {
		notifier, err := cluster.NewNotifier(d.State(), d.endpoints.NetworkCert(), cluster.NotifyAll)
		if err != nil {
			return SmartError(err)
		}
		err = notifier(func(client lxd.ContainerServer) error {
			_, _, err := client.GetServer()
			if err != nil {
				return err
			}
			return client.DeleteStoragePool(poolName)
		})
		if err != nil {
			return SmartError(err)
		}
	}

	err = dbStoragePoolDeleteAndUpdateCache(d.cluster, poolName)
	if err != nil {
		return SmartError(err)
	}

	return EmptySyncResponse
}

var storagePoolCmd = Command{name: "storage-pools/{name}", get: storagePoolGet, put: storagePoolPut, patch: storagePoolPatch, delete: storagePoolDelete}
<|endoftext|>


<commit_before>package markdown

import (
	"fmt"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/brandur/sorg"
	"github.com/brandur/sorg/templatehelpers"
	"github.com/russross/blackfriday"
)

var renderFuncs = []func(string, *RenderOptions) string{
	// pre-transformations
	transformFigures,
	transformHeaders,

	// main Markdown rendering
	renderMarkdown,

	// post-transformations
	transformCodeWithLanguagePrefix,
	transformSections,
	transformFootnotes,
	transformImagesToAbsoluteURLs,
	transformImagesToRetina,
}

// RenderOptions describes a rendering operation to be customized.
type RenderOptions struct {
	// AbsoluteURLs replaces the sources of any images that pointed to relative
	// URLs with absolute URLs.
	AbsoluteURLs bool

	// NoFootnoteLinks disables linking to and from footnotes.
	NoFootnoteLinks bool

	// NoHeaderLinks disables automatic permalinks on headers.
	NoHeaderLinks bool

	// NoRetina disables the Retina.JS rendering attributes.
	NoRetina bool
}

// Render a Markdown string to HTML while applying all custom project-specific
// filters including footnotes and stable header links.
func Render(source string, options *RenderOptions) string {
	for _, f := range renderFuncs {
		source = f(source, options)
	}
	return source
}

// Look for any whitespace between HTML tags.
var whitespaceRE = regexp.MustCompile(`>\s+<`)

// Simply collapses certain HTML snippets by removing newlines and whitespace
// between tags. This is mainline used to make HTML snippets readable as
// constants, but then to make them fit a little more nicely into the rendered
// markup.
func collapseHTML(html string) string {
	html = strings.Replace(html, "\n", "", -1)
	html = whitespaceRE.ReplaceAllString(html, "><")
	return html
}

func renderMarkdown(source string, options *RenderOptions) string {
	return string(blackfriday.Run([]byte(source)))
}

var codeRE = regexp.MustCompile(`<code class="(\w+)">`)

func transformCodeWithLanguagePrefix(source string, options *RenderOptions) string {
	return codeRE.ReplaceAllString(source, `<code class="language-$1">`)
}

const openSectionHTML = `<section class="%s">`
const closeSectionHTML = `</section>`

var openSectionRE = regexp.MustCompile(`(<p>)?!section class=("|“)(.*)("|”)(</p>)?`)
var closeSectionRE = regexp.MustCompile(`(<p>)?!/section(</p>)?`)

func transformSections(source string, options *RenderOptions) string {
	out := source

	out = openSectionRE.ReplaceAllStringFunc(out, func(div string) string {
		matches := openSectionRE.FindStringSubmatch(div)
		class := matches[3]
		return fmt.Sprintf(openSectionHTML, class)
	})
	out = closeSectionRE.ReplaceAllString(out, closeSectionHTML)

	return out
}

const figureHTML = `

%s

`

var figureRE = regexp.MustCompile(`!fig src="(.*)" caption="(.*)"`)

func transformFigures(source string, options *RenderOptions) string {
	return figureRE.ReplaceAllStringFunc(source, func(figure string) string {
		matches := figureRE.FindStringSubmatch(figure)
		src := matches[1]

		link := src
		extension := filepath.Ext(link)
		if extension != "" && extension != ".svg" {
			link = link[0:len(src)-len(extension)] + "@2x" + extension
		}

		// This is a really ugly hack in that it relies on the regex above
		// being greedy about quotes, but meh, I'll make it better when there's
		// a good reason to.
		caption := strings.Replace(matches[2], `\"`, `"`, -1)

		return fmt.Sprintf(figureHTML, link, src, caption)
	})
}

// A layer that we wrap the entire footer section in for styling purposes.
const footerWrapper = `

  %s

`

// HTML for a footnote within the document.
const footnoteAnchorHTML = `

  %s

`

// Same as footnoteAnchorHTML but without a link(this is used when sending
// emails).
const footnoteAnchorHTMLWithoutLink = `<sup><strong>%s</strong></sup>`

// HTML for a reference to a footnote within the document.
//
// Make sure there's a single space before the  because we're replacing
// one as part of our search.
const footnoteReferenceHTML = `

  %s

`

// Same as footnoteReferenceHTML but without a link (this is used when sending
// emails).
//
// Make sure there's a single space before the  because we're replacing
// one as part of our search.
const footnoteReferenceHTMLWithoutLink = ` <sup><strong>%s</strong></sup>`

// Look for the section the section at the bottom of the page that looks like
//
[1] (the paragraph tag is there because Markdown will have already
// wrapped it by this point).
var footerRE = regexp.MustCompile(`(?ms:^<p>\[\d+\].*)`)

// Look for a single footnote within the footer.
var footnoteRE = regexp.MustCompile(`\[(\d+)\](\s+.*)`)

// Note that this must be a post-transform filter. If it wasn't, our Markdown
// renderer would not render the Markdown inside the footnotes layer because it
// would already be wrapped in HTML.
func transformFootnotes(source string, options *RenderOptions) string {
	footer := footerRE.FindString(source)

	if footer != "" {
		// remove the footer for now
		source = strings.Replace(source, footer, "", 1)

		footer = footnoteRE.ReplaceAllStringFunc(footer, func(footnote string) string {
			// first create a footnote with an anchor that links can target
			matches := footnoteRE.FindStringSubmatch(footnote)
			number := matches[1]

			var anchor string
			if options != nil && options.NoFootnoteLinks {
				anchor = fmt.Sprintf(footnoteAnchorHTMLWithoutLink, number) + matches[2]
			} else {
				anchor = fmt.Sprintf(footnoteAnchorHTML, number, number, number) + matches[2]
			}

			// Then replace all references in the body to this footnote.
			//
			// Note the leading space before ` [%s]`. This is a little hacky,
			// but is there to try and ensure that we don't try to replace
			// strings that look like footnote references, but aren't.
			// `KEYS[1]` from `/redis-cluster` is an example of one of these
			// strings that might be a false positive.
			var reference string
			if options != nil && options.NoFootnoteLinks {
				reference = fmt.Sprintf(footnoteReferenceHTMLWithoutLink, number)
			} else {
				reference = fmt.Sprintf(footnoteReferenceHTML, number, number, number)
			}
			source = strings.Replace(source,
				fmt.Sprintf(` [%s]`, number),
				collapseHTML(reference), -1)

			return collapseHTML(anchor)
		})

		// and wrap the whole footer section in a layer for styling
		footer = fmt.Sprintf(footerWrapper, footer)
		source = source + footer
	}

	return source
}

const headerHTML = `
<h%v id="%s">
  %s
</h%v>
`

const headerHTMLNoLink = `
<h%v>%s</h%v>
`

// Matches one of the following:
//
//   # header
//   # header (#header-id)
//
// For now, only match ## or more so as to remove code comments from
// matches. We need a better way of doing that though.
var headerRE = regexp.MustCompile(`(?m:^(#{2,})\s+(.*?)(\s+\(#(.*)\))?$)`)

func transformHeaders(source string, options *RenderOptions) string {
	headerNum := 0

	// Tracks previously assigned headers so that we can detect duplicates.
	headers := make(map[string]int)

	source = headerRE.ReplaceAllStringFunc(source, func(header string) string {
		matches := headerRE.FindStringSubmatch(header)

		level := len(matches[1])
		title := matches[2]
		id := matches[4]

		var newID string

		if id == "" {
			// Header with no name, assign a prefixed number.
			newID = fmt.Sprintf("section-%v", headerNum)

		} else {
			occurrence, ok := headers[id]

			if ok {
				// Give duplicate IDs a suffix.
				newID = fmt.Sprintf("%s-%d", id, occurrence)
				headers[id]++

			} else {
				// Otherwise this is the first such ID we've seen.
				newID = id
				headers[id] = 1
			}
		}

		headerNum++

		// Replace the Markdown header with HTML equivalent.
		if options != nil && options.NoHeaderLinks {
			return collapseHTML(fmt.Sprintf(headerHTMLNoLink, level, title, level))
		}

		return collapseHTML(fmt.Sprintf(headerHTML, level, newID, newID, title, level))

	})

	return source
}

var imageRE = regexp.MustCompile(`<img src="(.+)"`)

func transformImagesToRetina(source string, options *RenderOptions) string {
	if options != nil && options.NoRetina {
		return source
	}

	// The basic idea here is that we give every image a `retina-rjs` tag so
	// that Retina.JS will replace it with a retina version *except* if the
	// image is an SVG. These are resolution agnostic and don't need replacing.
	return imageRE.ReplaceAllStringFunc(source, func(img string) string {
		matches := imageRE.FindStringSubmatch(img)
		if filepath.Ext(matches[1]) == ".svg" {
			return fmt.Sprintf(`<img src="%s"`, matches[1])
		}
		return fmt.Sprintf(`<img src="%s" srcset="%s 2x, %s 1x"`,
			matches[1],
			templatehelpers.To2x(matches[1]),
			matches[1],
		)
	})
}

var relativeImageRE = regexp.MustCompile(`<img src="/`)

func transformImagesToAbsoluteURLs(source string, options *RenderOptions) string {
	if options == nil || !options.AbsoluteURLs {
		return source
	}

	return relativeImageRE.ReplaceAllStringFunc(source, func(img string) string {
		return `<img src="` + sorg.AbsoluteURL + `/`
	})
}
<commit_msg>Fix image attribute matching<commit_after>package markdown

import (
	"fmt"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/brandur/sorg"
	"github.com/brandur/sorg/templatehelpers"
	"github.com/russross/blackfriday"
)

var renderFuncs = []func(string, *RenderOptions) string{
	// pre-transformations
	transformFigures,
	transformHeaders,

	// main Markdown rendering
	renderMarkdown,

	// post-transformations
	transformCodeWithLanguagePrefix,
	transformSections,
	transformFootnotes,
	transformImagesToAbsoluteURLs,
	transformImagesToRetina,
}

// RenderOptions describes a rendering operation to be customized.
type RenderOptions struct {
	// AbsoluteURLs replaces the sources of any images that pointed to relative
	// URLs with absolute URLs.
	AbsoluteURLs bool

	// NoFootnoteLinks disables linking to and from footnotes.
	NoFootnoteLinks bool

	// NoHeaderLinks disables automatic permalinks on headers.
	NoHeaderLinks bool

	// NoRetina disables the Retina.JS rendering attributes.
	NoRetina bool
}

// Render a Markdown string to HTML while applying all custom project-specific
// filters including footnotes and stable header links.
func Render(source string, options *RenderOptions) string {
	for _, f := range renderFuncs {
		source = f(source, options)
	}
	return source
}

// Look for any whitespace between HTML tags.
var whitespaceRE = regexp.MustCompile(`>\s+<`)

// Simply collapses certain HTML snippets by removing newlines and whitespace
// between tags. This is mainline used to make HTML snippets readable as
// constants, but then to make them fit a little more nicely into the rendered
// markup.
func collapseHTML(html string) string {
	html = strings.Replace(html, "\n", "", -1)
	html = whitespaceRE.ReplaceAllString(html, "><")
	return html
}

func renderMarkdown(source string, options *RenderOptions) string {
	return string(blackfriday.Run([]byte(source)))
}

var codeRE = regexp.MustCompile(`<code class="(\w+)">`)

func transformCodeWithLanguagePrefix(source string, options *RenderOptions) string {
	return codeRE.ReplaceAllString(source, `<code class="language-$1">`)
}

const openSectionHTML = `<section class="%s">`
const closeSectionHTML = `</section>`

var openSectionRE = regexp.MustCompile(`(<p>)?!section class=("|“)(.*)("|”)(</p>)?`)
var closeSectionRE = regexp.MustCompile(`(<p>)?!/section(</p>)?`)

func transformSections(source string, options *RenderOptions) string {
	out := source

	out = openSectionRE.ReplaceAllStringFunc(out, func(div string) string {
		matches := openSectionRE.FindStringSubmatch(div)
		class := matches[3]
		return fmt.Sprintf(openSectionHTML, class)
	})
	out = closeSectionRE.ReplaceAllString(out, closeSectionHTML)

	return out
}

const figureHTML = `

%s

`

var figureRE = regexp.MustCompile(`!fig src="(.*)" caption="(.*)"`)

func transformFigures(source string, options *RenderOptions) string {
	return figureRE.ReplaceAllStringFunc(source, func(figure string) string {
		matches := figureRE.FindStringSubmatch(figure)
		src := matches[1]

		link := src
		extension := filepath.Ext(link)
		if extension != "" && extension != ".svg" {
			link = link[0:len(src)-len(extension)] + "@2x" + extension
		}

		// This is a really ugly hack in that it relies on the regex above
		// being greedy about quotes, but meh, I'll make it better when there's
		// a good reason to.
		caption := strings.Replace(matches[2], `\"`, `"`, -1)

		return fmt.Sprintf(figureHTML, link, src, caption)
	})
}

// A layer that we wrap the entire footer section in for styling purposes.
const footerWrapper = `

  %s

`

// HTML for a footnote within the document.
const footnoteAnchorHTML = `

  %s

`

// Same as footnoteAnchorHTML but without a link(this is used when sending
// emails).
const footnoteAnchorHTMLWithoutLink = `<sup><strong>%s</strong></sup>`

// HTML for a reference to a footnote within the document.
//
// Make sure there's a single space before the  because we're replacing
// one as part of our search.
const footnoteReferenceHTML = `

  %s

`

// Same as footnoteReferenceHTML but without a link (this is used when sending
// emails).
//
// Make sure there's a single space before the  because we're replacing
// one as part of our search.
const footnoteReferenceHTMLWithoutLink = ` <sup><strong>%s</strong></sup>`

// Look for the section the section at the bottom of the page that looks like
//
[1] (the paragraph tag is there because Markdown will have already
// wrapped it by this point).
var footerRE = regexp.MustCompile(`(?ms:^<p>\[\d+\].*)`)

// Look for a single footnote within the footer.
var footnoteRE = regexp.MustCompile(`\[(\d+)\](\s+.*)`)

// Note that this must be a post-transform filter. If it wasn't, our Markdown
// renderer would not render the Markdown inside the footnotes layer because it
// would already be wrapped in HTML.
func transformFootnotes(source string, options *RenderOptions) string {
	footer := footerRE.FindString(source)

	if footer != "" {
		// remove the footer for now
		source = strings.Replace(source, footer, "", 1)

		footer = footnoteRE.ReplaceAllStringFunc(footer, func(footnote string) string {
			// first create a footnote with an anchor that links can target
			matches := footnoteRE.FindStringSubmatch(footnote)
			number := matches[1]

			var anchor string
			if options != nil && options.NoFootnoteLinks {
				anchor = fmt.Sprintf(footnoteAnchorHTMLWithoutLink, number) + matches[2]
			} else {
				anchor = fmt.Sprintf(footnoteAnchorHTML, number, number, number) + matches[2]
			}

			// Then replace all references in the body to this footnote.
			//
			// Note the leading space before ` [%s]`. This is a little hacky,
			// but is there to try and ensure that we don't try to replace
			// strings that look like footnote references, but aren't.
			// `KEYS[1]` from `/redis-cluster` is an example of one of these
			// strings that might be a false positive.
			var reference string
			if options != nil && options.NoFootnoteLinks {
				reference = fmt.Sprintf(footnoteReferenceHTMLWithoutLink, number)
			} else {
				reference = fmt.Sprintf(footnoteReferenceHTML, number, number, number)
			}
			source = strings.Replace(source,
				fmt.Sprintf(` [%s]`, number),
				collapseHTML(reference), -1)

			return collapseHTML(anchor)
		})

		// and wrap the whole footer section in a layer for styling
		footer = fmt.Sprintf(footerWrapper, footer)
		source = source + footer
	}

	return source
}

const headerHTML = `
<h%v id="%s">
  %s
</h%v>
`

const headerHTMLNoLink = `
<h%v>%s</h%v>
`

// Matches one of the following:
//
//   # header
//   # header (#header-id)
//
// For now, only match ## or more so as to remove code comments from
// matches. We need a better way of doing that though.
var headerRE = regexp.MustCompile(`(?m:^(#{2,})\s+(.*?)(\s+\(#(.*)\))?$)`)

func transformHeaders(source string, options *RenderOptions) string {
	headerNum := 0

	// Tracks previously assigned headers so that we can detect duplicates.
	headers := make(map[string]int)

	source = headerRE.ReplaceAllStringFunc(source, func(header string) string {
		matches := headerRE.FindStringSubmatch(header)

		level := len(matches[1])
		title := matches[2]
		id := matches[4]

		var newID string

		if id == "" {
			// Header with no name, assign a prefixed number.
			newID = fmt.Sprintf("section-%v", headerNum)

		} else {
			occurrence, ok := headers[id]

			if ok {
				// Give duplicate IDs a suffix.
				newID = fmt.Sprintf("%s-%d", id, occurrence)
				headers[id]++

			} else {
				// Otherwise this is the first such ID we've seen.
				newID = id
				headers[id] = 1
			}
		}

		headerNum++

		// Replace the Markdown header with HTML equivalent.
		if options != nil && options.NoHeaderLinks {
			return collapseHTML(fmt.Sprintf(headerHTMLNoLink, level, title, level))
		}

		return collapseHTML(fmt.Sprintf(headerHTML, level, newID, newID, title, level))

	})

	return source
}

var imageRE = regexp.MustCompile(`<img src="([^"]+)"`)

func transformImagesToRetina(source string, options *RenderOptions) string {
	if options != nil && options.NoRetina {
		return source
	}

	// The basic idea here is that we give every image a `srcset` that includes
	// 2x so that browsers will replace it with a retina version *except* if
	// the image is an SVG. These are resolution agnostic and don't need
	// replacing.
	return imageRE.ReplaceAllStringFunc(source, func(img string) string {
		matches := imageRE.FindStringSubmatch(img)
		if filepath.Ext(matches[1]) == ".svg" {
			return fmt.Sprintf(`<img src="%s"`, matches[1])
		}
		return fmt.Sprintf(`<img src="%s" srcset="%s 2x, %s 1x"`,
			matches[1],
			templatehelpers.To2x(matches[1]),
			matches[1],
		)
	})
}

var relativeImageRE = regexp.MustCompile(`<img src="/`)

func transformImagesToAbsoluteURLs(source string, options *RenderOptions) string {
	if options == nil || !options.AbsoluteURLs {
		return source
	}

	return relativeImageRE.ReplaceAllStringFunc(source, func(img string) string {
		return `<img src="` + sorg.AbsoluteURL + `/`
	})
}
<|endoftext|>