<commit_before>/*
 * Mini Object Storage, (C) 2014 Minio, 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 minioapi

import (
	"bytes"
	"encoding/json"
	"encoding/xml"
	"log"
	"net/http"
	"strconv"
	"time"

	"github.com/gorilla/mux"
	mstorage "github.com/minio-io/minio/pkg/storage"
)

type contentType int

const (
	xmlType contentType = iota
	jsonType
)

const (
	dateFormat = "2006-01-02T15:04:05.000Z"
)

type minioApi struct {
	storage mstorage.Storage
}

// No encoder interface exists, so we create one.
type encoder interface {
	Encode(v interface{}) error
}

func HttpHandler(storage mstorage.Storage) http.Handler {
	mux := mux.NewRouter()
	var api = minioApi{}
	api.storage = storage

	mux.HandleFunc("/", api.listBucketsHandler).Methods("GET")
	mux.HandleFunc("/{bucket}/", api.listObjectsHandler).Methods("GET")
	mux.HandleFunc("/{bucket}/", api.putBucketHandler).Methods("PUT")
	mux.HandleFunc("/{bucket}/{object:.*}", api.getObjectHandler).Methods("GET")
	mux.HandleFunc("/{bucket}/{object:.*}", api.headObjectHandler).Methods("HEAD")
	mux.HandleFunc("/{bucket}/{object:.*}", api.putObjectHandler).Methods("PUT")

	return mux
}

func (server *minioApi) ignoreUnImplementedBucketResources(req *http.Request) bool {
	q := req.URL.Query()
	for name := range q {
		if unimplementedBucketResourceNames[name] {
			return true
		}
	}
	return false
}

func (server *minioApi) ignoreUnImplementedObjectResources(req *http.Request) bool {
	q := req.URL.Query()
	for name := range q {
		if unimplementedObjectResourceNames[name] {
			return true
		}
	}
	return false
}

func (server *minioApi) getObjectHandler(w http.ResponseWriter, req *http.Request) {
	if server.ignoreUnImplementedObjectResources(req) {
		w.WriteHeader(http.StatusNotImplemented)
		return
	}

	vars := mux.Vars(req)
	bucket := vars["bucket"]
	object := vars["object"]

	metadata, err := server.storage.GetObjectMetadata(bucket, object)
	switch err := err.(type) {
	case nil: // success
		{
			log.Println("Found: " + bucket + "#" + object)
			writeObjectHeaders(w, metadata)
			if _, err := server.storage.CopyObjectToWriter(w, bucket, object); err != nil {
				log.Println(err)
			}
		}
	case mstorage.ObjectNotFound:
		{
			log.Println(err)
			w.WriteHeader(http.StatusNotFound)
		}
	default:
		{
			log.Println(err)
			w.WriteHeader(http.StatusInternalServerError)
		}
	}
}

func (server *minioApi) headObjectHandler(w http.ResponseWriter, req *http.Request) {
	vars := mux.Vars(req)
	bucket := vars["bucket"]
	object := vars["object"]

	metadata, err := server.storage.GetObjectMetadata(bucket, object)
	switch err := err.(type) {
	case nil:
		writeObjectHeaders(w, metadata)
	case mstorage.ObjectNotFound:
		log.Println(err)
		w.WriteHeader(http.StatusNotFound)
	default:
		log.Println(err)
		w.WriteHeader(http.StatusBadRequest)
	}
}

func (server *minioApi) listBucketsHandler(w http.ResponseWriter, req *http.Request) {
	if server.ignoreUnImplementedBucketResources(req) {
		w.WriteHeader(http.StatusNotImplemented)
		return
	}

	vars := mux.Vars(req)
	prefix, ok := vars["prefix"]
	if !ok {
		prefix = ""
	}

	contentType := xmlType
	if _, ok := req.Header["Accept"]; ok {
		if req.Header["Accept"][0] == "application/json" {
			contentType = jsonType
		}
	}
	buckets := server.storage.ListBuckets(prefix)
	response := generateBucketsListResult(buckets)

	var bytesBuffer bytes.Buffer
	var encoder encoder
	if contentType == xmlType {
		w.Header().Set("Content-Type", "application/xml")
		encoder = xml.NewEncoder(&bytesBuffer)
	} else if contentType == jsonType {
		w.Header().Set("Content-Type", "application/json")
		encoder = json.NewEncoder(&bytesBuffer)
	}
	encoder.Encode(response)
	w.Write(bytesBuffer.Bytes())
}

func (server *minioApi) listObjectsHandler(w http.ResponseWriter, req *http.Request) {
	if server.ignoreUnImplementedObjectResources(req) {
		w.WriteHeader(http.StatusNotImplemented)
		return
	}

	vars := mux.Vars(req)
	bucket := vars["bucket"]
	prefix, ok := vars["prefix"]
	if !ok {
		prefix = ""
	}

	contentType := xmlType
	if _, ok := req.Header["Accept"]; ok {
		if req.Header["Accept"][0] == "application/json" {
			contentType = jsonType
		}
	}

	objects := server.storage.ListObjects(bucket, prefix, 1000)
	response := generateObjectsListResult(bucket, objects)

	var bytesBuffer bytes.Buffer
	var encoder encoder
	if contentType == xmlType {
		w.Header().Set("Content-Type", "application/xml")
		encoder = xml.NewEncoder(&bytesBuffer)
	} else if contentType == jsonType {
		w.Header().Set("Content-Type", "application/json")
		encoder = json.NewEncoder(&bytesBuffer)
	}

	encoder.Encode(response)
	w.Write(bytesBuffer.Bytes())
}

func (server *minioApi) putObjectHandler(w http.ResponseWriter, req *http.Request) {
	if server.ignoreUnImplementedBucketResources(req) {
		w.WriteHeader(http.StatusNotImplemented)
		return
	}

	vars := mux.Vars(req)
	bucket := vars["bucket"]
	object := vars["object"]
	err := server.storage.StoreObject(bucket, object, req.Body)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte(err.Error()))
		return
	}
}

func (server *minioApi) putBucketHandler(w http.ResponseWriter, req *http.Request) {
	if server.ignoreUnImplementedBucketResources(req) {
		w.WriteHeader(http.StatusNotImplemented)
		return
	}

	vars := mux.Vars(req)
	bucket := vars["bucket"]
	err := server.storage.StoreBucket(bucket)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte(err.Error()))
		return
	}
}

// Write Object Header helper
func writeObjectHeaders(w http.ResponseWriter, metadata mstorage.ObjectMetadata) {
	lastModified := metadata.Created.Format(time.RFC1123)
	w.Header().Set("ETag", metadata.ETag)
	w.Header().Set("Last-Modified", lastModified)
	w.Header().Set("Content-Length", strconv.Itoa(metadata.Size))
	w.Header().Set("Content-Type", "text/plain")
}

func generateBucketsListResult(buckets []mstorage.BucketMetadata) BucketListResponse {
	var listbuckets []*Bucket
	var data = BucketListResponse{}
	var owner = Owner{}

	owner.ID = "minio"
	owner.DisplayName = "minio"

	for _, bucket := range buckets {
		var listbucket = &Bucket{}
		listbucket.Name = bucket.Name
		listbucket.CreationDate = bucket.Created.Format(dateFormat)
		listbuckets = append(listbuckets, listbucket)
	}

	data.Owner = owner
	data.Buckets.Bucket = listbuckets

	return data
}

func generateObjectsListResult(bucket string, objects []mstorage.ObjectMetadata) ObjectListResponse {
	var contents []*Item
	var owner = Owner{}
	var data = ObjectListResponse{}

	owner.ID = "minio"
	owner.DisplayName = "minio"

	for _, object := range objects {
		var content = &Item{}
		content.Key = object.Key
		content.LastModified = object.Created.Format(dateFormat)
		content.ETag = object.ETag
		content.Size = object.Size
		content.StorageClass = "STANDARD"
		content.Owner = owner
		contents = append(contents, content)
	}
	data.Name = bucket
	data.Contents = contents
	data.MaxKeys = MAX_OBJECT_LIST
	data.IsTruncated = false
	return data
}
<commit_msg>For listobjects ignore resources should be for buckets<commit_after>/*
 * Mini Object Storage, (C) 2014 Minio, 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 minioapi

import (
	"bytes"
	"encoding/json"
	"encoding/xml"
	"log"
	"net/http"
	"strconv"
	"time"

	"github.com/gorilla/mux"
	mstorage "github.com/minio-io/minio/pkg/storage"
)

type contentType int

const (
	xmlType contentType = iota
	jsonType
)

const (
	dateFormat = "2006-01-02T15:04:05.000Z"
)

type minioApi struct {
	storage mstorage.Storage
}

// No encoder interface exists, so we create one.
type encoder interface {
	Encode(v interface{}) error
}

func HttpHandler(storage mstorage.Storage) http.Handler {
	mux := mux.NewRouter()
	var api = minioApi{}
	api.storage = storage

	mux.HandleFunc("/", api.listBucketsHandler).Methods("GET")
	mux.HandleFunc("/{bucket}/", api.listObjectsHandler).Methods("GET")
	mux.HandleFunc("/{bucket}/", api.putBucketHandler).Methods("PUT")
	mux.HandleFunc("/{bucket}/{object:.*}", api.getObjectHandler).Methods("GET")
	mux.HandleFunc("/{bucket}/{object:.*}", api.headObjectHandler).Methods("HEAD")
	mux.HandleFunc("/{bucket}/{object:.*}", api.putObjectHandler).Methods("PUT")

	return mux
}

func (server *minioApi) ignoreUnImplementedBucketResources(req *http.Request) bool {
	q := req.URL.Query()
	for name := range q {
		if unimplementedBucketResourceNames[name] {
			return true
		}
	}
	return false
}

func (server *minioApi) ignoreUnImplementedObjectResources(req *http.Request) bool {
	q := req.URL.Query()
	for name := range q {
		if unimplementedObjectResourceNames[name] {
			return true
		}
	}
	return false
}

func (server *minioApi) getObjectHandler(w http.ResponseWriter, req *http.Request) {
	if server.ignoreUnImplementedObjectResources(req) {
		w.WriteHeader(http.StatusNotImplemented)
		return
	}

	vars := mux.Vars(req)
	bucket := vars["bucket"]
	object := vars["object"]

	metadata, err := server.storage.GetObjectMetadata(bucket, object)
	switch err := err.(type) {
	case nil: // success
		{
			log.Println("Found: " + bucket + "#" + object)
			writeObjectHeaders(w, metadata)
			if _, err := server.storage.CopyObjectToWriter(w, bucket, object); err != nil {
				log.Println(err)
			}
		}
	case mstorage.ObjectNotFound:
		{
			log.Println(err)
			w.WriteHeader(http.StatusNotFound)
		}
	default:
		{
			log.Println(err)
			w.WriteHeader(http.StatusInternalServerError)
		}
	}
}

func (server *minioApi) headObjectHandler(w http.ResponseWriter, req *http.Request) {
	vars := mux.Vars(req)
	bucket := vars["bucket"]
	object := vars["object"]

	metadata, err := server.storage.GetObjectMetadata(bucket, object)
	switch err := err.(type) {
	case nil:
		writeObjectHeaders(w, metadata)
	case mstorage.ObjectNotFound:
		log.Println(err)
		w.WriteHeader(http.StatusNotFound)
	default:
		log.Println(err)
		w.WriteHeader(http.StatusBadRequest)
	}
}

func (server *minioApi) listBucketsHandler(w http.ResponseWriter, req *http.Request) {
	if server.ignoreUnImplementedBucketResources(req) {
		w.WriteHeader(http.StatusNotImplemented)
		return
	}

	vars := mux.Vars(req)
	prefix, ok := vars["prefix"]
	if !ok {
		prefix = ""
	}

	contentType := xmlType
	if _, ok := req.Header["Accept"]; ok {
		if req.Header["Accept"][0] == "application/json" {
			contentType = jsonType
		}
	}
	buckets := server.storage.ListBuckets(prefix)
	response := generateBucketsListResult(buckets)

	var bytesBuffer bytes.Buffer
	var encoder encoder
	if contentType == xmlType {
		w.Header().Set("Content-Type", "application/xml")
		encoder = xml.NewEncoder(&bytesBuffer)
	} else if contentType == jsonType {
		w.Header().Set("Content-Type", "application/json")
		encoder = json.NewEncoder(&bytesBuffer)
	}
	encoder.Encode(response)
	w.Write(bytesBuffer.Bytes())
}

func (server *minioApi) listObjectsHandler(w http.ResponseWriter, req *http.Request) {
	if server.ignoreUnImplementedBucketResources(req) {
		w.WriteHeader(http.StatusNotImplemented)
		return
	}

	vars := mux.Vars(req)
	bucket := vars["bucket"]
	prefix, ok := vars["prefix"]
	if !ok {
		prefix = ""
	}

	contentType := xmlType
	if _, ok := req.Header["Accept"]; ok {
		if req.Header["Accept"][0] == "application/json" {
			contentType = jsonType
		}
	}

	objects := server.storage.ListObjects(bucket, prefix, 1000)
	response := generateObjectsListResult(bucket, objects)

	var bytesBuffer bytes.Buffer
	var encoder encoder
	if contentType == xmlType {
		w.Header().Set("Content-Type", "application/xml")
		encoder = xml.NewEncoder(&bytesBuffer)
	} else if contentType == jsonType {
		w.Header().Set("Content-Type", "application/json")
		encoder = json.NewEncoder(&bytesBuffer)
	}

	encoder.Encode(response)
	w.Write(bytesBuffer.Bytes())
}

func (server *minioApi) putObjectHandler(w http.ResponseWriter, req *http.Request) {
	if server.ignoreUnImplementedBucketResources(req) {
		w.WriteHeader(http.StatusNotImplemented)
		return
	}

	vars := mux.Vars(req)
	bucket := vars["bucket"]
	object := vars["object"]
	err := server.storage.StoreObject(bucket, object, req.Body)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte(err.Error()))
		return
	}
}

func (server *minioApi) putBucketHandler(w http.ResponseWriter, req *http.Request) {
	if server.ignoreUnImplementedBucketResources(req) {
		w.WriteHeader(http.StatusNotImplemented)
		return
	}

	vars := mux.Vars(req)
	bucket := vars["bucket"]
	err := server.storage.StoreBucket(bucket)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte(err.Error()))
		return
	}
}

// Write Object Header helper
func writeObjectHeaders(w http.ResponseWriter, metadata mstorage.ObjectMetadata) {
	lastModified := metadata.Created.Format(time.RFC1123)
	w.Header().Set("ETag", metadata.ETag)
	w.Header().Set("Last-Modified", lastModified)
	w.Header().Set("Content-Length", strconv.Itoa(metadata.Size))
	w.Header().Set("Content-Type", "text/plain")
}

func generateBucketsListResult(buckets []mstorage.BucketMetadata) BucketListResponse {
	var listbuckets []*Bucket
	var data = BucketListResponse{}
	var owner = Owner{}

	owner.ID = "minio"
	owner.DisplayName = "minio"

	for _, bucket := range buckets {
		var listbucket = &Bucket{}
		listbucket.Name = bucket.Name
		listbucket.CreationDate = bucket.Created.Format(dateFormat)
		listbuckets = append(listbuckets, listbucket)
	}

	data.Owner = owner
	data.Buckets.Bucket = listbuckets

	return data
}

func generateObjectsListResult(bucket string, objects []mstorage.ObjectMetadata) ObjectListResponse {
	var contents []*Item
	var owner = Owner{}
	var data = ObjectListResponse{}

	owner.ID = "minio"
	owner.DisplayName = "minio"

	for _, object := range objects {
		var content = &Item{}
		content.Key = object.Key
		content.LastModified = object.Created.Format(dateFormat)
		content.ETag = object.ETag
		content.Size = object.Size
		content.StorageClass = "STANDARD"
		content.Owner = owner
		contents = append(contents, content)
	}
	data.Name = bucket
	data.Contents = contents
	data.MaxKeys = MAX_OBJECT_LIST
	data.IsTruncated = false
	return data
}
<|endoftext|>


<commit_before>//  Copyright (c) 2014 Couchbase, 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 main

import (
	"encoding/json"
	"fmt"
	"io"

	"github.com/blevesearch/bleve"
)

func init() {
	// Register alias with empty instantiation functions,
	// so that "alias" will show up in valid index types.
	RegisterPIndexImplType("alias", &PIndexImplType{
		Validate:    ValidateAlias,
		Count:       CountAlias,
		Query:       QueryAlias,
		Description: "alias - supports fan-out of queries to multiple index targets",
		StartSample: &AliasParams{
			Targets: map[string]*AliasParamsTarget{
				"yourIndexName": &AliasParamsTarget{},
			},
		},
	})
}

// AliasParams holds the definition for a user-defined index alias.  A
// user-defined index alias can be used as a level of indirection (the
// "LastQuartersSales" alias points currently to the "2014-Q3-Sales"
// index, but the administrator might repoint it in the future without
// changing the application) or to scatter-gather or fan-out a query
// across multiple real indexes (e.g., to query across customer
// records, product catalog, call-center records, etc, in one shot).
type AliasParams struct {
	Targets map[string]*AliasParamsTarget `json:"targets"` // Keyed by indexName.
}

type AliasParamsTarget struct {
	IndexUUID string `json:"indexUUID"` // Optional.
}

func ValidateAlias(indexType, indexName, indexParams string) error {
	params := AliasParams{}
	return json.Unmarshal([]byte(indexParams), &params)
}

func CountAlias(mgr *Manager, indexName, indexUUID string) (uint64, error) {
	alias, err := bleveIndexAliasForUserIndexAlias(mgr,
		indexName, indexUUID, nil, nil)
	if err != nil {
		return 0, fmt.Errorf("CountAlias indexAlias error,"+
			" indexName: %s, indexUUID: %s, err: %v", indexName, indexUUID, err)
	}

	return alias.DocCount()
}

func QueryAlias(mgr *Manager, indexName, indexUUID string,
	req []byte, res io.Writer) error {
	var bleveQueryParams BleveQueryParams
	err := json.Unmarshal(req, &bleveQueryParams)
	if err != nil {
		return fmt.Errorf("QueryAlias parsing bleveQueryParams, err: %v", err)
	}

	alias, err := bleveIndexAliasForUserIndexAlias(mgr, indexName, indexUUID,
		bleveQueryParams.Consistency, nil)
	if err != nil {
		return fmt.Errorf("QueryAlias indexAlias error,"+
			" indexName: %s, indexUUID: %s, err: %v", indexName, indexUUID, err)
	}

	err = bleveQueryParams.Query.Query.Validate()
	if err != nil {
		return err
	}

	searchResponse, err := alias.Search(bleveQueryParams.Query)
	if err != nil {
		return err
	}

	mustEncode(res, searchResponse)

	return nil
}

// The indexName/indexUUID is for a user-defined index alias.
//
// TODO: One day support user-defined aliases for non-bleve indexes.
func bleveIndexAliasForUserIndexAlias(mgr *Manager,
	indexName, indexUUID string, consistencyParams *ConsistencyParams,
	cancelCh chan struct{}) (
	bleve.IndexAlias, error) {
	alias := bleve.NewIndexAlias()

	indexDefs, _, err := CfgGetIndexDefs(mgr.cfg)
	if err != nil {
		return nil, fmt.Errorf("could not get indexDefs, indexName: %s, err: %v",
			indexName, err)
	}

	num := 0

	var fillAlias func(aliasName, aliasUUID string) error

	fillAlias = func(aliasName, aliasUUID string) error {
		aliasDef := indexDefs.IndexDefs[aliasName]
		if aliasDef == nil {
			return fmt.Errorf("could not get aliasDef, aliasName: %s, indexName: %s",
				aliasName, indexName)
		}
		if aliasDef.Type != "alias" {
			return fmt.Errorf("not alias type: %s, aliasName: %s, indexName: %s",
				aliasDef.Type, aliasName, indexName)
		}
		if aliasUUID != "" &&
			aliasUUID != aliasDef.UUID {
			return fmt.Errorf("mismatched aliasUUID: %s, aliasDef.UUID: %s,"+
				" aliasName: %s, indexName: %s", aliasUUID, aliasDef.UUID,
				aliasName, indexName)
		}

		params := AliasParams{}
		err := json.Unmarshal([]byte(aliasDef.Params), &params)
		if err != nil {
			return fmt.Errorf("could not parse aliasDef.Params: %s, aliasName: %s,"+
				" indexName: %s", aliasDef.Params, aliasName, indexName)
		}

		for targetName, targetSpec := range params.Targets {
			if num > 50000 {
				return fmt.Errorf("too many alias targets,"+
					" perhaps there's a cycle, aliasName: %s, indexName: %s",
					aliasName, indexName)
			}
			targetDef := indexDefs.IndexDefs[targetName]
			if targetDef == nil {
				return fmt.Errorf("no indexDef for targetName: %s, aliasName: %s,"+
					" indexName: %s", targetName, aliasName, indexName)
			}
			if targetSpec.IndexUUID != "" &&
				targetSpec.IndexUUID != targetDef.UUID {
				return fmt.Errorf("mismatched targetSpec.UUID: %s, targetDef.UUID: %s,"+
					" targetName: %s, aliasName: %s, indexName: %s",
					targetSpec.IndexUUID, targetDef.UUID, targetName, aliasName, indexName)
			}

			// TODO: Perhaps convert to registered callbacks instead of if-else-if.
			if targetDef.Type == "alias" {
				err = fillAlias(targetName, targetSpec.IndexUUID)
				if err != nil {
					return err
				}
			} else if targetDef.Type == "bleve" {
				subAlias, err := bleveIndexAlias(mgr, targetName,
					targetSpec.IndexUUID, consistencyParams, cancelCh)
				if err != nil {
					return fmt.Errorf("bleveIndexAlias, indexName: %s,"+
						" targetName: %s, targetSpec: %#v, err: %v",
						indexName, targetName, targetSpec, err)
				}
				alias.Add(subAlias)
				num += 1
			} else {
				return fmt.Errorf("unsupported alias target type: %s,"+
					" targetName: %s, aliasName: %s, indexName: %s",
					targetDef.Type, targetName, aliasName, indexName)
			}
		}

		return nil
	}

	err = fillAlias(indexName, indexUUID)
	if err != nil {
		return nil, err
	}

	return alias, nil
}
<commit_msg>maxAliasTargets instead of hardcoded constant<commit_after>//  Copyright (c) 2014 Couchbase, 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 main

import (
	"encoding/json"
	"fmt"
	"io"

	"github.com/blevesearch/bleve"
)

var maxAliasTargets = 50000

func init() {
	// Register alias with empty instantiation functions,
	// so that "alias" will show up in valid index types.
	RegisterPIndexImplType("alias", &PIndexImplType{
		Validate:    ValidateAlias,
		Count:       CountAlias,
		Query:       QueryAlias,
		Description: "alias - supports fan-out of queries to multiple index targets",
		StartSample: &AliasParams{
			Targets: map[string]*AliasParamsTarget{
				"yourIndexName": &AliasParamsTarget{},
			},
		},
	})
}

// AliasParams holds the definition for a user-defined index alias.  A
// user-defined index alias can be used as a level of indirection (the
// "LastQuartersSales" alias points currently to the "2014-Q3-Sales"
// index, but the administrator might repoint it in the future without
// changing the application) or to scatter-gather or fan-out a query
// across multiple real indexes (e.g., to query across customer
// records, product catalog, call-center records, etc, in one shot).
type AliasParams struct {
	Targets map[string]*AliasParamsTarget `json:"targets"` // Keyed by indexName.
}

type AliasParamsTarget struct {
	IndexUUID string `json:"indexUUID"` // Optional.
}

func ValidateAlias(indexType, indexName, indexParams string) error {
	params := AliasParams{}
	return json.Unmarshal([]byte(indexParams), &params)
}

func CountAlias(mgr *Manager, indexName, indexUUID string) (uint64, error) {
	alias, err := bleveIndexAliasForUserIndexAlias(mgr,
		indexName, indexUUID, nil, nil)
	if err != nil {
		return 0, fmt.Errorf("CountAlias indexAlias error,"+
			" indexName: %s, indexUUID: %s, err: %v", indexName, indexUUID, err)
	}

	return alias.DocCount()
}

func QueryAlias(mgr *Manager, indexName, indexUUID string,
	req []byte, res io.Writer) error {
	var bleveQueryParams BleveQueryParams
	err := json.Unmarshal(req, &bleveQueryParams)
	if err != nil {
		return fmt.Errorf("QueryAlias parsing bleveQueryParams, err: %v", err)
	}

	alias, err := bleveIndexAliasForUserIndexAlias(mgr, indexName, indexUUID,
		bleveQueryParams.Consistency, nil)
	if err != nil {
		return fmt.Errorf("QueryAlias indexAlias error,"+
			" indexName: %s, indexUUID: %s, err: %v", indexName, indexUUID, err)
	}

	err = bleveQueryParams.Query.Query.Validate()
	if err != nil {
		return err
	}

	searchResponse, err := alias.Search(bleveQueryParams.Query)
	if err != nil {
		return err
	}

	mustEncode(res, searchResponse)

	return nil
}

// The indexName/indexUUID is for a user-defined index alias.
//
// TODO: One day support user-defined aliases for non-bleve indexes.
func bleveIndexAliasForUserIndexAlias(mgr *Manager,
	indexName, indexUUID string, consistencyParams *ConsistencyParams,
	cancelCh chan struct{}) (
	bleve.IndexAlias, error) {
	alias := bleve.NewIndexAlias()

	indexDefs, _, err := CfgGetIndexDefs(mgr.cfg)
	if err != nil {
		return nil, fmt.Errorf("could not get indexDefs, indexName: %s, err: %v",
			indexName, err)
	}

	num := 0

	var fillAlias func(aliasName, aliasUUID string) error

	fillAlias = func(aliasName, aliasUUID string) error {
		aliasDef := indexDefs.IndexDefs[aliasName]
		if aliasDef == nil {
			return fmt.Errorf("could not get aliasDef, aliasName: %s, indexName: %s",
				aliasName, indexName)
		}
		if aliasDef.Type != "alias" {
			return fmt.Errorf("not alias type: %s, aliasName: %s, indexName: %s",
				aliasDef.Type, aliasName, indexName)
		}
		if aliasUUID != "" &&
			aliasUUID != aliasDef.UUID {
			return fmt.Errorf("mismatched aliasUUID: %s, aliasDef.UUID: %s,"+
				" aliasName: %s, indexName: %s", aliasUUID, aliasDef.UUID,
				aliasName, indexName)
		}

		params := AliasParams{}
		err := json.Unmarshal([]byte(aliasDef.Params), &params)
		if err != nil {
			return fmt.Errorf("could not parse aliasDef.Params: %s, aliasName: %s,"+
				" indexName: %s", aliasDef.Params, aliasName, indexName)
		}

		for targetName, targetSpec := range params.Targets {
			if num > maxAliasTargets {
				return fmt.Errorf("too many alias targets,"+
					" perhaps there's a cycle, aliasName: %s, indexName: %s",
					aliasName, indexName)
			}
			targetDef := indexDefs.IndexDefs[targetName]
			if targetDef == nil {
				return fmt.Errorf("no indexDef for targetName: %s, aliasName: %s,"+
					" indexName: %s", targetName, aliasName, indexName)
			}
			if targetSpec.IndexUUID != "" &&
				targetSpec.IndexUUID != targetDef.UUID {
				return fmt.Errorf("mismatched targetSpec.UUID: %s, targetDef.UUID: %s,"+
					" targetName: %s, aliasName: %s, indexName: %s",
					targetSpec.IndexUUID, targetDef.UUID, targetName, aliasName, indexName)
			}

			// TODO: Perhaps convert to registered callbacks instead of if-else-if.
			if targetDef.Type == "alias" {
				err = fillAlias(targetName, targetSpec.IndexUUID)
				if err != nil {
					return err
				}
			} else if targetDef.Type == "bleve" {
				subAlias, err := bleveIndexAlias(mgr, targetName,
					targetSpec.IndexUUID, consistencyParams, cancelCh)
				if err != nil {
					return fmt.Errorf("bleveIndexAlias, indexName: %s,"+
						" targetName: %s, targetSpec: %#v, err: %v",
						indexName, targetName, targetSpec, err)
				}
				alias.Add(subAlias)
				num += 1
			} else {
				return fmt.Errorf("unsupported alias target type: %s,"+
					" targetName: %s, aliasName: %s, indexName: %s",
					targetDef.Type, targetName, aliasName, indexName)
			}
		}

		return nil
	}

	err = fillAlias(indexName, indexUUID)
	if err != nil {
		return nil, err
	}

	return alias, nil
}
<|endoftext|>


<commit_before>// Copyright 2012 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package keychain implements access to the passwords and other keys
// stored in the system-provided keychain.
package keychain

// BUG(rsc): Package keychain is only implemented on OS X.

// UserPasswd returns the user name and password for authenticating
// to the named server.  If the user argument is non-empty, UserPasswd
// restricts its search to passwords for the named user.
func UserPasswd(server, preferredUser string) (user, passwd string, err error) {
	return userPasswd(server, preferredUser)
}

<commit_msg>keychain: better error from UserPasswd<commit_after>// Copyright 2012 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package keychain implements access to the passwords and other keys
// stored in the system-provided keychain.
package keychain

// BUG(rsc): Package keychain is only implemented on OS X.

import (
	"fmt"
)

// UserPasswd returns the user name and password for authenticating
// to the named server.  If the user argument is non-empty, UserPasswd
// restricts its search to passwords for the named user.
func UserPasswd(server, preferredUser string) (user, passwd string, err error) {
	user, passwd, err = userPasswd(server, preferredUser)
	if err != nil {
		if preferredUser != "" {
			err = fmt.Errorf("loading password for %s@%s: %v", preferredUser, server, err)
		} else {
			err = fmt.Errorf("loading password for %s: %v", server, err)
		}
	}
	return
}

<|endoftext|>


<commit_before>// Copyright 2016-2019 Authors of Cilium
//
// 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 client

import (
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"sort"
	"strings"
	"text/tabwriter"
	"time"

	clientapi "github.com/cilium/cilium/api/v1/client"
	"github.com/cilium/cilium/api/v1/models"
	"github.com/cilium/cilium/pkg/defaults"
	"github.com/cilium/cilium/pkg/ip"

	runtime_client "github.com/go-openapi/runtime/client"
	"github.com/go-openapi/strfmt"
)

type Client struct {
	clientapi.Cilium
}

// DefaultSockPath returns deafult UNIX domain socket path or
// path set using CILIUM_SOCK env variable
func DefaultSockPath() string {
	// Check if environment variable points to socket
	e := os.Getenv(defaults.SockPathEnv)
	if e == "" {
		// If unset, fall back to default value
		e = defaults.SockPath
	}
	return "unix://" + e

}

func configureTransport(tr *http.Transport, proto, addr string) *http.Transport {
	if tr == nil {
		tr = &http.Transport{}
	}

	if proto == "unix" {
		// No need for compression in local communications.
		tr.DisableCompression = true
		tr.Dial = func(_, _ string) (net.Conn, error) {
			return net.Dial(proto, addr)
		}
	} else {
		tr.Proxy = http.ProxyFromEnvironment
		tr.Dial = (&net.Dialer{}).Dial
	}

	return tr
}

// NewDefaultClient creates a client with default parameters connecting to UNIX domain socket.
func NewDefaultClient() (*Client, error) {
	return NewClient("")
}

// NewDefaultClientWithTimeout creates a client with default parameters connecting to UNIX
// domain socket and waits for cilium-agent availability.
func NewDefaultClientWithTimeout(timeout time.Duration) (*Client, error) {
	timeoutAfter := time.After(timeout)
	var c *Client
	var err error
	for {
		select {
		case <-timeoutAfter:
			return c, fmt.Errorf("Failed to create cilium agent client after %f seconds timeout: %s", timeout.Seconds(), err)
		default:
		}

		c, err = NewDefaultClient()
		if err == nil {
			break
		}
	}

	return c, nil
}

// NewClient creates a client for the given `host`.
// If host is nil then use SockPath provided by CILIUM_SOCK
// or the cilium default SockPath
func NewClient(host string) (*Client, error) {
	if host == "" {
		host = DefaultSockPath()
	}
	tmp := strings.SplitN(host, "://", 2)
	if len(tmp) != 2 {
		return nil, fmt.Errorf("invalid host format '%s'", host)
	}

	switch tmp[0] {
	case "tcp":
		if _, err := url.Parse("tcp://" + tmp[1]); err != nil {
			return nil, err
		}
		host = "http://" + tmp[1]
	case "unix":
		host = tmp[1]
	}

	transport := configureTransport(nil, tmp[0], host)
	httpClient := &http.Client{Transport: transport}
	clientTrans := runtime_client.NewWithClient(tmp[1], clientapi.DefaultBasePath,
		clientapi.DefaultSchemes, httpClient)
	return &Client{*clientapi.New(clientTrans, strfmt.Default)}, nil
}

// Hint tries to improve the error message displayed to the user.
func Hint(err error) error {
	if err == nil {
		return err
	}
	e, _ := url.PathUnescape(err.Error())
	if strings.Contains(err.Error(), defaults.SockPath) {
		return fmt.Errorf("%s\nIs the agent running?", e)
	}
	return fmt.Errorf("%s", e)
}

func timeSince(since time.Time) string {
	out := "never"
	if !since.IsZero() {
		// Poor man's implementtion of time.Truncate(). Can be refined
		// when we rebase to go 1.9
		t := time.Since(since)
		t -= t % time.Second
		out = t.String() + " ago"
	}

	return out
}

func stateUnhealthy(state string) bool {
	return state == models.StatusStateWarning ||
		state == models.StatusStateFailure
}

func statusUnhealthy(s *models.Status) bool {
	if s != nil {
		return stateUnhealthy(s.State)
	}
	return false
}

// FormatStatusResponseBrief writes a one-line status to the writer. If
// everything ok, this is "ok", otherwise a message of the form "error in ..."
func FormatStatusResponseBrief(w io.Writer, sr *models.StatusResponse) {
	msg := ""

	switch {
	case statusUnhealthy(sr.Cilium):
		msg = fmt.Sprintf("cilium: %s", sr.Cilium.Msg)
	case statusUnhealthy(sr.ContainerRuntime):
		msg = fmt.Sprintf("container runtime: %s", sr.ContainerRuntime.Msg)
	case statusUnhealthy(sr.Kvstore):
		msg = fmt.Sprintf("kvstore: %s", sr.Kvstore.Msg)
	case sr.Kubernetes != nil && stateUnhealthy(sr.Kubernetes.State):
		msg = fmt.Sprintf("kubernetes: %s", sr.Kubernetes.Msg)
	case sr.Cluster != nil && statusUnhealthy(sr.Cluster.CiliumHealth):
		msg = fmt.Sprintf("cilium-health: %s", sr.Cluster.CiliumHealth.Msg)
	}

	// Only bother looking at controller failures if everything else is ok
	if msg == "" {
		for _, ctrl := range sr.Controllers {
			if ctrl.Status == nil {
				continue
			}
			if ctrl.Status.LastFailureMsg != "" {
				msg = fmt.Sprintf("controller %s: %s",
					ctrl.Name, ctrl.Status.LastFailureMsg)
				break
			}
		}
	}

	if msg == "" {
		fmt.Fprintf(w, "OK\n")
	} else {
		fmt.Fprintf(w, "error in %s\n", msg)
	}
}

// FormatStatusResponse writes a StatusResponse as a string to the writer.
//
// The parameters 'allAddresses', 'allControllers', 'allNodes', respectively,
// cause all details about that aspect of the status to be printed to the
// terminal. For each of these, if they are false then only a summary will be
// printed, with perhaps some detail if there are errors.
func FormatStatusResponse(w io.Writer, sr *models.StatusResponse, allAddresses, allControllers, allNodes, allRedirects bool) {
	if sr.Kvstore != nil {
		fmt.Fprintf(w, "KVStore:\t%s\t%s\n", sr.Kvstore.State, sr.Kvstore.Msg)
	}
	if sr.ContainerRuntime != nil {
		fmt.Fprintf(w, "ContainerRuntime:\t%s\t%s\n",
			sr.ContainerRuntime.State, sr.ContainerRuntime.Msg)
	}
	if sr.Kubernetes != nil {
		fmt.Fprintf(w, "Kubernetes:\t%s\t%s\n", sr.Kubernetes.State, sr.Kubernetes.Msg)
		if sr.Kubernetes.State != models.K8sStatusStateDisabled {
			sort.Strings(sr.Kubernetes.K8sAPIVersions)
			fmt.Fprintf(w, "Kubernetes APIs:\t[\"%s\"]\n", strings.Join(sr.Kubernetes.K8sAPIVersions, "\", \""))
		}
	}
	if sr.Cilium != nil {
		fmt.Fprintf(w, "Cilium:\t%s\t%s\n", sr.Cilium.State, sr.Cilium.Msg)
	}

	if sr.Stale != nil {
		sortedProbes := make([]string, 0, len(sr.Stale))
		for probe := range sr.Stale {
			sortedProbes = append(sortedProbes, probe)
		}
		sort.Strings(sortedProbes)

		stalesStr := make([]string, 0, len(sr.Stale))
		for _, probe := range sortedProbes {
			stalesStr = append(stalesStr, fmt.Sprintf("%q since %s", probe, sr.Stale[probe]))
		}

		fmt.Fprintf(w, "Stale status:\t%s\n", strings.Join(stalesStr, ", "))
	}

	if nm := sr.NodeMonitor; nm != nil {
		fmt.Fprintf(w, "NodeMonitor:\tListening for events on %d CPUs with %dx%d of shared memory\n",
			nm.Cpus, nm.Npages, nm.Pagesize)
		if nm.Lost != 0 || nm.Unknown != 0 {
			fmt.Fprintf(w, "\t%d events lost, %d unknown notifications\n", nm.Lost, nm.Unknown)
		}
	} else {
		fmt.Fprintf(w, "NodeMonitor:\tDisabled\n")
	}

	var localNode *models.NodeElement
	if sr.Cluster != nil {
		if sr.Cluster.CiliumHealth != nil {
			ch := sr.Cluster.CiliumHealth
			fmt.Fprintf(w, "Cilium health daemon:\t%s\t%s\n", ch.State, ch.Msg)
		}
		for _, node := range sr.Cluster.Nodes {
			if node.Name == sr.Cluster.Self {
				localNode = node
			} else {
				continue
			}
		}
	}

	if sr.IPAM != nil {
		var v4CIDR, v6CIDR string
		v4AllocRange := localNode.PrimaryAddress.IPV4.AllocRange
		v6AllocRange := localNode.PrimaryAddress.IPV6.AllocRange
		if localNode != nil {
			if nIPs := ip.CountIPsInCIDR(v4AllocRange); nIPs > 0 {
				v4CIDR = fmt.Sprintf("/%d", nIPs)
			}
			if nIPs := ip.CountIPsInCIDR(v6AllocRange); nIPs > 0 {
				v6CIDR = fmt.Sprintf("/%d", nIPs)
			}
		}
		fmt.Fprintf(w, "IPv4 address pool:\t%d%s allocated from %s\n", len(sr.IPAM.IPV4), v4CIDR, v4AllocRange)
		if allAddresses {
			for _, ipv4 := range sr.IPAM.IPV4 {
				fmt.Fprintf(w, "  %s\n", ipv4)
			}
		}
		fmt.Fprintf(w, "IPv6 address pool:\t%d%s allocated from %s\n", len(sr.IPAM.IPV6), v6CIDR, v6AllocRange)
		if allAddresses {
			for _, ipv6 := range sr.IPAM.IPV6 {
				fmt.Fprintf(w, "  %s\n", ipv6)
			}
		}
	}

	if sr.Controllers != nil {
		nFailing, out := 0, []string{"  Name\tLast success\tLast error\tCount\tMessage\n"}
		for _, ctrl := range sr.Controllers {
			status := ctrl.Status
			if status == nil {
				continue
			}

			if status.ConsecutiveFailureCount > 0 {
				nFailing++
			} else if !allControllers {
				continue
			}

			failSince := timeSince(time.Time(status.LastFailureTimestamp))
			successSince := timeSince(time.Time(status.LastSuccessTimestamp))

			err := "no error"
			if status.LastFailureMsg != "" {
				err = status.LastFailureMsg
			}

			out = append(out, fmt.Sprintf("  %s\t%s\t%s\t%d\t%s\t\n",
				ctrl.Name, successSince, failSince, status.ConsecutiveFailureCount, err))
		}

		nOK := len(sr.Controllers) - nFailing
		fmt.Fprintf(w, "Controller Status:\t%d/%d healthy\n", nOK, len(sr.Controllers))
		if len(out) > 1 {
			tab := tabwriter.NewWriter(w, 0, 0, 3, ' ', 0)
			sort.Strings(out)
			for _, s := range out {
				fmt.Fprint(tab, s)
			}
			tab.Flush()
		}

	}

	if sr.Proxy != nil {
		fmt.Fprintf(w, "Proxy Status:\tOK, ip %s, port-range %s\n",
			sr.Proxy.IP, sr.Proxy.PortRange)
	} else {
		fmt.Fprintf(w, "Proxy Status:\tNo managed proxy redirect\n")
	}
}
<commit_msg>client: Fix a possible NULL pointer dereference of localNode<commit_after>// Copyright 2016-2019 Authors of Cilium
//
// 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 client

import (
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"os"
	"sort"
	"strings"
	"text/tabwriter"
	"time"

	clientapi "github.com/cilium/cilium/api/v1/client"
	"github.com/cilium/cilium/api/v1/models"
	"github.com/cilium/cilium/pkg/defaults"
	"github.com/cilium/cilium/pkg/ip"

	runtime_client "github.com/go-openapi/runtime/client"
	"github.com/go-openapi/strfmt"
)

type Client struct {
	clientapi.Cilium
}

// DefaultSockPath returns deafult UNIX domain socket path or
// path set using CILIUM_SOCK env variable
func DefaultSockPath() string {
	// Check if environment variable points to socket
	e := os.Getenv(defaults.SockPathEnv)
	if e == "" {
		// If unset, fall back to default value
		e = defaults.SockPath
	}
	return "unix://" + e

}

func configureTransport(tr *http.Transport, proto, addr string) *http.Transport {
	if tr == nil {
		tr = &http.Transport{}
	}

	if proto == "unix" {
		// No need for compression in local communications.
		tr.DisableCompression = true
		tr.Dial = func(_, _ string) (net.Conn, error) {
			return net.Dial(proto, addr)
		}
	} else {
		tr.Proxy = http.ProxyFromEnvironment
		tr.Dial = (&net.Dialer{}).Dial
	}

	return tr
}

// NewDefaultClient creates a client with default parameters connecting to UNIX domain socket.
func NewDefaultClient() (*Client, error) {
	return NewClient("")
}

// NewDefaultClientWithTimeout creates a client with default parameters connecting to UNIX
// domain socket and waits for cilium-agent availability.
func NewDefaultClientWithTimeout(timeout time.Duration) (*Client, error) {
	timeoutAfter := time.After(timeout)
	var c *Client
	var err error
	for {
		select {
		case <-timeoutAfter:
			return c, fmt.Errorf("Failed to create cilium agent client after %f seconds timeout: %s", timeout.Seconds(), err)
		default:
		}

		c, err = NewDefaultClient()
		if err == nil {
			break
		}
	}

	return c, nil
}

// NewClient creates a client for the given `host`.
// If host is nil then use SockPath provided by CILIUM_SOCK
// or the cilium default SockPath
func NewClient(host string) (*Client, error) {
	if host == "" {
		host = DefaultSockPath()
	}
	tmp := strings.SplitN(host, "://", 2)
	if len(tmp) != 2 {
		return nil, fmt.Errorf("invalid host format '%s'", host)
	}

	switch tmp[0] {
	case "tcp":
		if _, err := url.Parse("tcp://" + tmp[1]); err != nil {
			return nil, err
		}
		host = "http://" + tmp[1]
	case "unix":
		host = tmp[1]
	}

	transport := configureTransport(nil, tmp[0], host)
	httpClient := &http.Client{Transport: transport}
	clientTrans := runtime_client.NewWithClient(tmp[1], clientapi.DefaultBasePath,
		clientapi.DefaultSchemes, httpClient)
	return &Client{*clientapi.New(clientTrans, strfmt.Default)}, nil
}

// Hint tries to improve the error message displayed to the user.
func Hint(err error) error {
	if err == nil {
		return err
	}
	e, _ := url.PathUnescape(err.Error())
	if strings.Contains(err.Error(), defaults.SockPath) {
		return fmt.Errorf("%s\nIs the agent running?", e)
	}
	return fmt.Errorf("%s", e)
}

func timeSince(since time.Time) string {
	out := "never"
	if !since.IsZero() {
		// Poor man's implementtion of time.Truncate(). Can be refined
		// when we rebase to go 1.9
		t := time.Since(since)
		t -= t % time.Second
		out = t.String() + " ago"
	}

	return out
}

func stateUnhealthy(state string) bool {
	return state == models.StatusStateWarning ||
		state == models.StatusStateFailure
}

func statusUnhealthy(s *models.Status) bool {
	if s != nil {
		return stateUnhealthy(s.State)
	}
	return false
}

// FormatStatusResponseBrief writes a one-line status to the writer. If
// everything ok, this is "ok", otherwise a message of the form "error in ..."
func FormatStatusResponseBrief(w io.Writer, sr *models.StatusResponse) {
	msg := ""

	switch {
	case statusUnhealthy(sr.Cilium):
		msg = fmt.Sprintf("cilium: %s", sr.Cilium.Msg)
	case statusUnhealthy(sr.ContainerRuntime):
		msg = fmt.Sprintf("container runtime: %s", sr.ContainerRuntime.Msg)
	case statusUnhealthy(sr.Kvstore):
		msg = fmt.Sprintf("kvstore: %s", sr.Kvstore.Msg)
	case sr.Kubernetes != nil && stateUnhealthy(sr.Kubernetes.State):
		msg = fmt.Sprintf("kubernetes: %s", sr.Kubernetes.Msg)
	case sr.Cluster != nil && statusUnhealthy(sr.Cluster.CiliumHealth):
		msg = fmt.Sprintf("cilium-health: %s", sr.Cluster.CiliumHealth.Msg)
	}

	// Only bother looking at controller failures if everything else is ok
	if msg == "" {
		for _, ctrl := range sr.Controllers {
			if ctrl.Status == nil {
				continue
			}
			if ctrl.Status.LastFailureMsg != "" {
				msg = fmt.Sprintf("controller %s: %s",
					ctrl.Name, ctrl.Status.LastFailureMsg)
				break
			}
		}
	}

	if msg == "" {
		fmt.Fprintf(w, "OK\n")
	} else {
		fmt.Fprintf(w, "error in %s\n", msg)
	}
}

// FormatStatusResponse writes a StatusResponse as a string to the writer.
//
// The parameters 'allAddresses', 'allControllers', 'allNodes', respectively,
// cause all details about that aspect of the status to be printed to the
// terminal. For each of these, if they are false then only a summary will be
// printed, with perhaps some detail if there are errors.
func FormatStatusResponse(w io.Writer, sr *models.StatusResponse, allAddresses, allControllers, allNodes, allRedirects bool) {
	if sr.Kvstore != nil {
		fmt.Fprintf(w, "KVStore:\t%s\t%s\n", sr.Kvstore.State, sr.Kvstore.Msg)
	}
	if sr.ContainerRuntime != nil {
		fmt.Fprintf(w, "ContainerRuntime:\t%s\t%s\n",
			sr.ContainerRuntime.State, sr.ContainerRuntime.Msg)
	}
	if sr.Kubernetes != nil {
		fmt.Fprintf(w, "Kubernetes:\t%s\t%s\n", sr.Kubernetes.State, sr.Kubernetes.Msg)
		if sr.Kubernetes.State != models.K8sStatusStateDisabled {
			sort.Strings(sr.Kubernetes.K8sAPIVersions)
			fmt.Fprintf(w, "Kubernetes APIs:\t[\"%s\"]\n", strings.Join(sr.Kubernetes.K8sAPIVersions, "\", \""))
		}
	}
	if sr.Cilium != nil {
		fmt.Fprintf(w, "Cilium:\t%s\t%s\n", sr.Cilium.State, sr.Cilium.Msg)
	}

	if sr.Stale != nil {
		sortedProbes := make([]string, 0, len(sr.Stale))
		for probe := range sr.Stale {
			sortedProbes = append(sortedProbes, probe)
		}
		sort.Strings(sortedProbes)

		stalesStr := make([]string, 0, len(sr.Stale))
		for _, probe := range sortedProbes {
			stalesStr = append(stalesStr, fmt.Sprintf("%q since %s", probe, sr.Stale[probe]))
		}

		fmt.Fprintf(w, "Stale status:\t%s\n", strings.Join(stalesStr, ", "))
	}

	if nm := sr.NodeMonitor; nm != nil {
		fmt.Fprintf(w, "NodeMonitor:\tListening for events on %d CPUs with %dx%d of shared memory\n",
			nm.Cpus, nm.Npages, nm.Pagesize)
		if nm.Lost != 0 || nm.Unknown != 0 {
			fmt.Fprintf(w, "\t%d events lost, %d unknown notifications\n", nm.Lost, nm.Unknown)
		}
	} else {
		fmt.Fprintf(w, "NodeMonitor:\tDisabled\n")
	}

	var localNode *models.NodeElement
	if sr.Cluster != nil {
		if sr.Cluster.CiliumHealth != nil {
			ch := sr.Cluster.CiliumHealth
			fmt.Fprintf(w, "Cilium health daemon:\t%s\t%s\n", ch.State, ch.Msg)
		}
		for _, node := range sr.Cluster.Nodes {
			if node.Name == sr.Cluster.Self {
				localNode = node
			} else {
				continue
			}
		}
	}

	if sr.IPAM != nil {
		var v4CIDR, v6CIDR, v4AllocRangeFmt, v6AllocRangeFmt string
		if localNode != nil {
			v4AllocRange := localNode.PrimaryAddress.IPV4.AllocRange
			v6AllocRange := localNode.PrimaryAddress.IPV6.AllocRange
			v4AllocRangeFmt = fmt.Sprintf(" allocated from %s", v4AllocRange)
			v6AllocRangeFmt = fmt.Sprintf(" allocated from %s", v6AllocRange)
			if nIPs := ip.CountIPsInCIDR(v4AllocRange); nIPs > 0 {
				v4CIDR = fmt.Sprintf("/%d", nIPs)
			}
			if nIPs := ip.CountIPsInCIDR(v6AllocRange); nIPs > 0 {
				v6CIDR = fmt.Sprintf("/%d", nIPs)
			}
		}
		fmt.Fprintf(w, "IPv4 address pool:\t%d%s%s\n", len(sr.IPAM.IPV4), v4CIDR, v4AllocRangeFmt)
		if allAddresses {
			for _, ipv4 := range sr.IPAM.IPV4 {
				fmt.Fprintf(w, "  %s\n", ipv4)
			}
		}
		fmt.Fprintf(w, "IPv6 address pool:\t%d%s%s\n", len(sr.IPAM.IPV6), v6CIDR, v6AllocRangeFmt)
		if allAddresses {
			for _, ipv6 := range sr.IPAM.IPV6 {
				fmt.Fprintf(w, "  %s\n", ipv6)
			}
		}
	}

	if sr.Controllers != nil {
		nFailing, out := 0, []string{"  Name\tLast success\tLast error\tCount\tMessage\n"}
		for _, ctrl := range sr.Controllers {
			status := ctrl.Status
			if status == nil {
				continue
			}

			if status.ConsecutiveFailureCount > 0 {
				nFailing++
			} else if !allControllers {
				continue
			}

			failSince := timeSince(time.Time(status.LastFailureTimestamp))
			successSince := timeSince(time.Time(status.LastSuccessTimestamp))

			err := "no error"
			if status.LastFailureMsg != "" {
				err = status.LastFailureMsg
			}

			out = append(out, fmt.Sprintf("  %s\t%s\t%s\t%d\t%s\t\n",
				ctrl.Name, successSince, failSince, status.ConsecutiveFailureCount, err))
		}

		nOK := len(sr.Controllers) - nFailing
		fmt.Fprintf(w, "Controller Status:\t%d/%d healthy\n", nOK, len(sr.Controllers))
		if len(out) > 1 {
			tab := tabwriter.NewWriter(w, 0, 0, 3, ' ', 0)
			sort.Strings(out)
			for _, s := range out {
				fmt.Fprint(tab, s)
			}
			tab.Flush()
		}

	}

	if sr.Proxy != nil {
		fmt.Fprintf(w, "Proxy Status:\tOK, ip %s, port-range %s\n",
			sr.Proxy.IP, sr.Proxy.PortRange)
	} else {
		fmt.Fprintf(w, "Proxy Status:\tNo managed proxy redirect\n")
	}
}
<|endoftext|>


<commit_before>// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://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 filter contains methods for selecting and filtering lists of
// components and objects.
package filter

import (
	"strings"

	bundle "github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/apis/bundle/v1alpha1"
	"github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/converter"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

// Filter filters the components and objects to produce a new set of components.
type Filter struct{}

// NewFilter creates a new Filter.
func NewFilter() *Filter {
	return &Filter{}
}

// Options for filtering bundles. By default, if any of the options match, then
// the relevant component or object is removed. If InvertMatch is set, then the
// objects are kept instead of removed.
type Options struct {
	// Kinds represent the Kinds to filter on. Can either be unqualified ("Deployment")
	// or qualified ("apps/v1beta1,Pod"). Qualified kinds are often called
	// GroupVersionKind in the Kubernetes Schema.
	Kinds []string

	// Names represent the names to filter on. For objects, this is the
	// metadata.name field. For components, this is the ComponentName.
	Names []string

	// Annotations contain key/value pairs to filter on. An empty string value matches
	// all annotation-values for a particular key.
	Annotations map[string]string

	// Labels contain key/value pairs to filter on. An empty string value matches
	// all label-values for a particular key.
	Labels map[string]string

	// Namespaces to filter on.
	Namespaces []string

	// InvertMatch indicates wether to return the opposite match.
	InvertMatch bool
}

// OptionsFromObjectSelector creates an Options Object from a
func OptionsFromObjectSelector(sel *bundle.ObjectSelector) *Options {
	// TODO(kashomon): Should probably make a copy.
	if sel == nil {
		return nil
	}

	opts := &Options{
		Kinds:       sel.Kinds,
		Names:       sel.Names,
		Annotations: sel.Annotations,
		Labels:      sel.Labels,
		Namespaces:  sel.Namespaces,
	}
	if sel.InvertMatch != nil {
		opts.InvertMatch = *sel.InvertMatch
	}
	return opts
}

// FilterComponents removes components based on the ObjectMeta properties of
// the components, returning a new cluster bundle with just filtered
// components. Filtering for components doesn't take into account the
// properties of the object-children of the components. This is the opposite
// matching from SelectComponents.
func (f *Filter) FilterComponents(data []*bundle.Component, o *Options) []*bundle.Component {
	matched, notMatched := f.PartitionComponents(data, o)
	if o.InvertMatch {
		return matched
	}
	return notMatched
}

// SelectComponents picks components based on the ObjectMeta properties of the
// components, returning a new cluster bundle with just filtered components.
// Filtering for components doesn't take into account the properties of the
// object-children of the components. This performs the opposte matching from
// FilterComponents.
func (f *Filter) SelectComponents(data []*bundle.Component, o *Options) []*bundle.Component {
	matched, notMatched := f.PartitionComponents(data, o)
	if o.InvertMatch {
		return notMatched
	}
	return matched
}

// PartitionComponents splits the components into matched and not matched sets.
// PartitionComponents ignores the InvertMatch option, since both matched and
// unmatched objects are returned. Thus, the options to partition are always
// treated as options for matching objects.
func (f *Filter) PartitionComponents(data []*bundle.Component, o *Options) ([]*bundle.Component, []*bundle.Component) {
	var newData []*bundle.Component
	for _, cp := range data {
		newData = append(newData, cp.DeepCopy())
	}
	data = newData

	// nil options should not imply any change.
	if o == nil {
		return data, nil
	}

	var matched []*bundle.Component
	var notMatched []*bundle.Component
	for _, c := range data {
		if MatchesComponent(c, o) {
			matched = append(matched, c)
		} else {
			notMatched = append(notMatched, c)
		}
	}
	return matched, notMatched
}

// FilterObjects removes objects based on the ObjectMeta properties of the objects,
// returning a new list with just filtered objects. This performs the opposite match from SelectObjects.
func (f *Filter) FilterObjects(data []*unstructured.Unstructured, o *Options) []*unstructured.Unstructured {
	matched, notMatched := f.PartitionObjects(data, o)
	if o.InvertMatch {
		return matched
	}
	return notMatched
}

// SelectObjects picks objects based on the ObjectMeta properties of the objects,
// returning a new list with just filtered objects.
func (f *Filter) SelectObjects(data []*unstructured.Unstructured, o *Options) []*unstructured.Unstructured {
	matched, notMatched := f.PartitionObjects(data, o)
	if o.InvertMatch {
		return notMatched
	}
	return matched
}

// PartitionObjects splits the objects into matched and not matched sets.
// PartitionObjects ignores the KeepOnly option, since both matched and
// unmatched objects are returned. Thus, the options to partition are always
// treated as options for matching objects.
func (f *Filter) PartitionObjects(data []*unstructured.Unstructured, o *Options) ([]*unstructured.Unstructured, []*unstructured.Unstructured) {
	var newData []*unstructured.Unstructured
	for _, oj := range data {
		newData = append(newData, oj.DeepCopy())
	}
	data = newData

	// nil options should not imply any change.
	if o == nil {
		return data, nil
	}

	var matched []*unstructured.Unstructured
	var notMatched []*unstructured.Unstructured
	for _, cp := range data {
		if MatchesObject(cp, o) {
			matched = append(matched, cp)
		} else {
			notMatched = append(notMatched, cp)
		}
	}
	return matched, notMatched
}

// objectData contains data about the object being filtered.
type objectData struct {
	// APIVersion of the object.
	apiVersion string

	// Kind of the object.
	kind string

	// name of the object. For unstructured objects, this is is metadata.name.
	// For components, this is ComponentName
	name string

	// meta is the ObjectMeta for an object.
	meta *metav1.ObjectMeta
}

// objectDataFromComponent returns ObjectData created from a component.
func objectDataFromComponent(c *bundle.Component) *objectData {
	return &objectData{
		apiVersion: c.APIVersion,
		kind:       c.Kind,
		name:       c.Spec.ComponentName,
		meta:       c.ObjectMeta.DeepCopy(),
	}
}

// newObjectData returns ObjectData created from an unstructured Object.
func newObjectData(uns *unstructured.Unstructured) *objectData {
	return &objectData{
		apiVersion: uns.GetAPIVersion(),
		kind:       uns.GetKind(),
		name:       uns.GetName(),
		meta:       converter.FromUnstructured(uns).ExtractObjectMeta(),
	}
}

// MatchesComponent returns true if the conditions match a Component.
func MatchesComponent(c *bundle.Component, o *Options) bool {
	return matches(objectDataFromComponent(c), o)
}

// MatchesObject returns true if the conditions match an object.
func MatchesObject(obj *unstructured.Unstructured, o *Options) bool {
	return matches(newObjectData(obj), o)
}

// Matches returns whether an object matches the given. The match functions
// does an AND of ORS. In otherwords:
//
// (name1 OR name2 OR name3) AND
// (kind2 OR kind2 OR kind3) AND etc.
func matches(d *objectData, o *Options) bool {
	if o == nil {
		return true
	}

	matchesKinds := true
	if len(o.Kinds) > 0 {
		matchesKinds = false
		for _, optk := range o.Kinds {
			objKind := d.kind
			if strings.ContainsRune(optk, ',') {
				// Assume this is a Qualified Kind match of the form
				// "apps/v1beta1,Deployment". Commas shouldn't be normally in a kind.
				objKind = d.apiVersion + "," + d.kind
			}
			if optk == objKind {
				matchesKinds = true
				break
			}
		}
	}
	matchesNS := true
	if len(o.Namespaces) > 0 {
		matchesNS = false
		for _, optn := range o.Namespaces {
			if optn == d.meta.Namespace {
				matchesNS = true
				break
			}
		}
	}
	matchesNames := true
	if len(o.Names) > 0 {
		matchesNames = false
		for _, optn := range o.Names {
			if optn == d.meta.GetName() {
				matchesNames = true
				break
			}
		}
	}
	matchesAnnot := true
	if len(o.Annotations) > 0 {
		matchesAnnot = false
		for key, v := range o.Annotations {
			if val, ok := d.meta.Annotations[key]; ok && val == v {
				matchesAnnot = true
				break
			}
		}
	}
	matchesLabels := true
	if len(o.Labels) > 0 {
		matchesLabels = false
		for key, v := range o.Labels {
			if val, ok := d.meta.Labels[key]; ok && val == v {
				matchesLabels = true
				break
			}
		}
	}
	return matchesKinds && matchesNS && matchesNames && matchesAnnot && matchesLabels
}

// ComponentPredicate is a func that returns true for components that match
// criteria and false otherwise.
type ComponentPredicate func(*bundle.Component) bool

// Select takes a list of components and a predicate and returns the
// components that match the predicate.
func Select(components []*bundle.Component, predicate ComponentPredicate) []*bundle.Component {
	var out []*bundle.Component
	for _, component := range components {
		if predicate(component) {
			out = append(out, component)
		}
	}
	return out
}

// ComponentFieldMatchIn takes a []string and see if the field matches one of
// those.
func ComponentFieldMatchIn(matchList []string, fieldGetter func(*bundle.Component) string) ComponentPredicate {
	return func(component *bundle.Component) bool {
		fv := fieldGetter(component)
		for _, match := range matchList {
			if fv == match {
				return true
			}
		}
		return false
	}
}

// ObjectFieldMatchIn takes a []string and sees if any object in the component
// matches one of those.
func ObjectFieldMatchIn(matchList []string, objectGetter func(*unstructured.Unstructured) string) ComponentPredicate {
	return func(component *bundle.Component) bool {
		for _, obj := range component.Spec.Objects {
			ov := objectGetter(obj)
			for _, match := range matchList {
				if ov == match {
					return true
				}
			}
		}
		return false
	}
}

// And returns true if every ComponentPredicate function returns true and
// returns false otherwise.
func And(componentPredicates ...ComponentPredicate) ComponentPredicate {
	return func(component *bundle.Component) bool {
		for _, predicate := range componentPredicates {
			if !predicate(component) {
				return false
			}
		}
		return true
	}
}

// Or returns true if any ComponentPredicate function returns true and
// returns false otherwise.
func Or(componentPredicates ...ComponentPredicate) ComponentPredicate {
	return func(component *bundle.Component) bool {
		for _, predicate := range componentPredicates {
			if predicate(component) {
				return true
			}
		}
		return false
	}
}

// Not returns a ComponentPredicate that negates it's result.
func Not(predicate ComponentPredicate) ComponentPredicate {
	return func(component *bundle.Component) bool {
		return !predicate(component)
	}
}

// SelectObjects returns components with only the objects that match the
// predicate.
func SelectObjects(components []*bundle.Component, predicate ComponentPredicate) []*bundle.Component {
	var out []*bundle.Component
	for _, component := range components {
		temp := component.DeepCopy()
		var selectedObjects []*unstructured.Unstructured
		for _, object := range component.Spec.Objects {
			temp.Spec.Objects = []*unstructured.Unstructured{object}
			if predicate(temp) {
				selectedObjects = append(selectedObjects, object)
			}
		}
		if len(selectedObjects) == 0 {
			continue
		}
		newComp := component.DeepCopy()
		newComp.Spec.Objects = selectedObjects
		out = append(out, newComp)
	}
	return out
}
<commit_msg>Support invert match in the internal function `matches`. (#310)<commit_after>// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://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 filter contains methods for selecting and filtering lists of
// components and objects.
package filter

import (
	"strings"

	bundle "github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/apis/bundle/v1alpha1"
	"github.com/GoogleCloudPlatform/k8s-cluster-bundle/pkg/converter"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

// Filter filters the components and objects to produce a new set of components.
type Filter struct{}

// NewFilter creates a new Filter.
func NewFilter() *Filter {
	return &Filter{}
}

// Options for filtering bundles. By default, if any of the options match, then
// the relevant component or object is removed. If InvertMatch is set, then the
// objects are kept instead of removed.
type Options struct {
	// Kinds represent the Kinds to filter on. Can either be unqualified ("Deployment")
	// or qualified ("apps/v1beta1,Pod"). Qualified kinds are often called
	// GroupVersionKind in the Kubernetes Schema.
	Kinds []string

	// Names represent the names to filter on. For objects, this is the
	// metadata.name field. For components, this is the ComponentName.
	Names []string

	// Annotations contain key/value pairs to filter on. An empty string value matches
	// all annotation-values for a particular key.
	Annotations map[string]string

	// Labels contain key/value pairs to filter on. An empty string value matches
	// all label-values for a particular key.
	Labels map[string]string

	// Namespaces to filter on.
	Namespaces []string

	// InvertMatch indicates wether to return the opposite match.
	InvertMatch bool
}

// OptionsFromObjectSelector creates an Options Object from a
func OptionsFromObjectSelector(sel *bundle.ObjectSelector) *Options {
	// TODO(kashomon): Should probably make a copy.
	if sel == nil {
		return nil
	}

	opts := &Options{
		Kinds:       sel.Kinds,
		Names:       sel.Names,
		Annotations: sel.Annotations,
		Labels:      sel.Labels,
		Namespaces:  sel.Namespaces,
	}
	if sel.InvertMatch != nil {
		opts.InvertMatch = *sel.InvertMatch
	}
	return opts
}

// FilterComponents removes components based on the ObjectMeta properties of
// the components, returning a new cluster bundle with just filtered
// components. Filtering for components doesn't take into account the
// properties of the object-children of the components. This is the opposite
// matching from SelectComponents.
func (f *Filter) FilterComponents(data []*bundle.Component, o *Options) []*bundle.Component {
	_, notMatched := f.PartitionComponents(data, o)
	return notMatched
}

// SelectComponents picks components based on the ObjectMeta properties of the
// components, returning a new cluster bundle with just filtered components.
// Filtering for components doesn't take into account the properties of the
// object-children of the components. This performs the opposte matching from
// FilterComponents.
func (f *Filter) SelectComponents(data []*bundle.Component, o *Options) []*bundle.Component {
	matched, _ := f.PartitionComponents(data, o)
	return matched
}

// PartitionComponents splits the components into matched and not matched sets.
// PartitionComponents ignores the InvertMatch option, since both matched and
// unmatched objects are returned. Thus, the options to partition are always
// treated as options for matching objects.
func (f *Filter) PartitionComponents(data []*bundle.Component, o *Options) ([]*bundle.Component, []*bundle.Component) {
	var newData []*bundle.Component
	for _, cp := range data {
		newData = append(newData, cp.DeepCopy())
	}
	data = newData

	// nil options should not imply any change.
	if o == nil {
		return data, nil
	}

	var matched []*bundle.Component
	var notMatched []*bundle.Component
	for _, c := range data {
		if MatchesComponent(c, o) {
			matched = append(matched, c)
		} else {
			notMatched = append(notMatched, c)
		}
	}
	return matched, notMatched
}

// FilterObjects removes objects based on the ObjectMeta properties of the objects,
// returning a new list with just filtered objects. This performs the opposite match from SelectObjects.
func (f *Filter) FilterObjects(data []*unstructured.Unstructured, o *Options) []*unstructured.Unstructured {
	_, notMatched := f.PartitionObjects(data, o)
	return notMatched
}

// SelectObjects picks objects based on the ObjectMeta properties of the objects,
// returning a new list with just filtered objects.
func (f *Filter) SelectObjects(data []*unstructured.Unstructured, o *Options) []*unstructured.Unstructured {
	matched, _ := f.PartitionObjects(data, o)
	return matched
}

// PartitionObjects splits the objects into matched and not matched sets.
// PartitionObjects ignores the KeepOnly option, since both matched and
// unmatched objects are returned. Thus, the options to partition are always
// treated as options for matching objects.
func (f *Filter) PartitionObjects(data []*unstructured.Unstructured, o *Options) ([]*unstructured.Unstructured, []*unstructured.Unstructured) {
	var newData []*unstructured.Unstructured
	for _, oj := range data {
		newData = append(newData, oj.DeepCopy())
	}
	data = newData

	// nil options should not imply any change.
	if o == nil {
		return data, nil
	}

	var matched []*unstructured.Unstructured
	var notMatched []*unstructured.Unstructured
	for _, cp := range data {
		if MatchesObject(cp, o) {
			matched = append(matched, cp)
		} else {
			notMatched = append(notMatched, cp)
		}
	}
	return matched, notMatched
}

// objectData contains data about the object being filtered.
type objectData struct {
	// APIVersion of the object.
	apiVersion string

	// Kind of the object.
	kind string

	// name of the object. For unstructured objects, this is is metadata.name.
	// For components, this is ComponentName
	name string

	// meta is the ObjectMeta for an object.
	meta *metav1.ObjectMeta
}

// objectDataFromComponent returns ObjectData created from a component.
func objectDataFromComponent(c *bundle.Component) *objectData {
	return &objectData{
		apiVersion: c.APIVersion,
		kind:       c.Kind,
		name:       c.Spec.ComponentName,
		meta:       c.ObjectMeta.DeepCopy(),
	}
}

// newObjectData returns ObjectData created from an unstructured Object.
func newObjectData(uns *unstructured.Unstructured) *objectData {
	return &objectData{
		apiVersion: uns.GetAPIVersion(),
		kind:       uns.GetKind(),
		name:       uns.GetName(),
		meta:       converter.FromUnstructured(uns).ExtractObjectMeta(),
	}
}

// MatchesComponent returns true if the conditions match a Component.
func MatchesComponent(c *bundle.Component, o *Options) bool {
	return matches(objectDataFromComponent(c), o)
}

// MatchesObject returns true if the conditions match an object.
func MatchesObject(obj *unstructured.Unstructured, o *Options) bool {
	return matches(newObjectData(obj), o)
}

// Matches returns whether an object matches the given. The match functions
// does an AND of ORS. In otherwords:
//
// (name1 OR name2 OR name3) AND
// (kind2 OR kind2 OR kind3) AND etc.
func matches(d *objectData, o *Options) bool {
	if o == nil {
		return true
	}

	matchesKinds := true
	if len(o.Kinds) > 0 {
		matchesKinds = false
		for _, optk := range o.Kinds {
			objKind := d.kind
			if strings.ContainsRune(optk, ',') {
				// Assume this is a Qualified Kind match of the form
				// "apps/v1beta1,Deployment". Commas shouldn't be normally in a kind.
				objKind = d.apiVersion + "," + d.kind
			}
			if optk == objKind {
				matchesKinds = true
				break
			}
		}
	}
	matchesNS := true
	if len(o.Namespaces) > 0 {
		matchesNS = false
		for _, optn := range o.Namespaces {
			if optn == d.meta.Namespace {
				matchesNS = true
				break
			}
		}
	}
	matchesNames := true
	if len(o.Names) > 0 {
		matchesNames = false
		for _, optn := range o.Names {
			if optn == d.meta.GetName() {
				matchesNames = true
				break
			}
		}
	}
	matchesAnnot := true
	if len(o.Annotations) > 0 {
		matchesAnnot = false
		for key, v := range o.Annotations {
			if val, ok := d.meta.Annotations[key]; ok && val == v {
				matchesAnnot = true
				break
			}
		}
	}
	matchesLabels := true
	if len(o.Labels) > 0 {
		matchesLabels = false
		for key, v := range o.Labels {
			if val, ok := d.meta.Labels[key]; ok && val == v {
				matchesLabels = true
				break
			}
		}
	}

	matches := matchesKinds && matchesNS && matchesNames && matchesAnnot && matchesLabels
	if o.InvertMatch {
		return !matches
	}
	return matches
}

// ComponentPredicate is a func that returns true for components that match
// criteria and false otherwise.
type ComponentPredicate func(*bundle.Component) bool

// Select takes a list of components and a predicate and returns the
// components that match the predicate.
func Select(components []*bundle.Component, predicate ComponentPredicate) []*bundle.Component {
	var out []*bundle.Component
	for _, component := range components {
		if predicate(component) {
			out = append(out, component)
		}
	}
	return out
}

// ComponentFieldMatchIn takes a []string and see if the field matches one of
// those.
func ComponentFieldMatchIn(matchList []string, fieldGetter func(*bundle.Component) string) ComponentPredicate {
	return func(component *bundle.Component) bool {
		fv := fieldGetter(component)
		for _, match := range matchList {
			if fv == match {
				return true
			}
		}
		return false
	}
}

// ObjectFieldMatchIn takes a []string and sees if any object in the component
// matches one of those.
func ObjectFieldMatchIn(matchList []string, objectGetter func(*unstructured.Unstructured) string) ComponentPredicate {
	return func(component *bundle.Component) bool {
		for _, obj := range component.Spec.Objects {
			ov := objectGetter(obj)
			for _, match := range matchList {
				if ov == match {
					return true
				}
			}
		}
		return false
	}
}

// And returns true if every ComponentPredicate function returns true and
// returns false otherwise.
func And(componentPredicates ...ComponentPredicate) ComponentPredicate {
	return func(component *bundle.Component) bool {
		for _, predicate := range componentPredicates {
			if !predicate(component) {
				return false
			}
		}
		return true
	}
}

// Or returns true if any ComponentPredicate function returns true and
// returns false otherwise.
func Or(componentPredicates ...ComponentPredicate) ComponentPredicate {
	return func(component *bundle.Component) bool {
		for _, predicate := range componentPredicates {
			if predicate(component) {
				return true
			}
		}
		return false
	}
}

// Not returns a ComponentPredicate that negates it's result.
func Not(predicate ComponentPredicate) ComponentPredicate {
	return func(component *bundle.Component) bool {
		return !predicate(component)
	}
}

// SelectObjects returns components with only the objects that match the
// predicate.
func SelectObjects(components []*bundle.Component, predicate ComponentPredicate) []*bundle.Component {
	var out []*bundle.Component
	for _, component := range components {
		temp := component.DeepCopy()
		var selectedObjects []*unstructured.Unstructured
		for _, object := range component.Spec.Objects {
			temp.Spec.Objects = []*unstructured.Unstructured{object}
			if predicate(temp) {
				selectedObjects = append(selectedObjects, object)
			}
		}
		if len(selectedObjects) == 0 {
			continue
		}
		newComp := component.DeepCopy()
		newComp.Spec.Objects = selectedObjects
		out = append(out, newComp)
	}
	return out
}
<|endoftext|>


<commit_before>package report

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"sync"
	"time"

	"github.com/pajbot/pajbot2/pkg"
)

type ReportUser struct {
	ID   string
	Name string
	Type string `json:",omitempty"`
}

func (u ReportUser) GetName() string {
	return u.Name
}

func (u ReportUser) GetID() string {
	return u.ID
}

type Report struct {
	ID       uint32
	Channel  ReportUser
	Reporter ReportUser
	Target   ReportUser
	Reason   string `json:",omitempty"`
	Logs     []string
	Time     time.Time
}

type HistoricReport struct {
	ID             uint32
	Channel        ReportUser
	Reporter       ReportUser
	Target         ReportUser
	Reason         string
	Logs           []string
	Time           time.Time
	Handler        ReportUser
	Action         uint8
	ActionDuration uint32
	TimeHandled    time.Time
}

type Holder struct {
	db           *sql.DB
	pubSub       pkg.PubSub
	userStore    pkg.UserStore
	channelStore pkg.ChannelStore
	botStore     pkg.BotStore

	reportsMutex *sync.Mutex
	reports      map[uint32]Report
}

var _ pkg.PubSubConnection = &Holder{}
var _ pkg.PubSubSource = &Holder{}
var _ pkg.PubSubSubscriptionHandler = &Holder{}

func New(app pkg.Application) (*Holder, error) {
	h := &Holder{
		db:           app.SQL(),
		pubSub:       app.PubSub(),
		userStore:    app.UserStore(),
		botStore:     app.TwitchBots(),
		channelStore: app.ChannelStore(),

		reportsMutex: &sync.Mutex{},
		reports:      make(map[uint32]Report),
	}

	err := h.Load()
	if err != nil {
		return nil, err
	}

	h.pubSub.Subscribe(h, "HandleReport", nil)
	h.pubSub.Subscribe(h, "TimeoutEvent", nil)
	h.pubSub.Subscribe(h, "BanEvent", nil)
	h.pubSub.HandleSubscribe(h, "ReportReceived")

	return h, nil
}

func (h *Holder) AuthenticatedUser() pkg.User {
	return nil
}

func (h *Holder) IsApplication() bool {
	return true
}

func (h *Holder) Connection() pkg.PubSubConnection {
	return h
}

func (h *Holder) Load() error {
	const queryF = `
SELECT
	id, channel_id, channel_name,
	channel_type, reporter_id, reporter_name,
	target_id, target_name,
	reason, logs, time
FROM
	report`

	rows, err := h.db.Query(queryF) // GOOD
	if err != nil {
		return err
	}
	defer rows.Close()

	h.reportsMutex.Lock()
	defer h.reportsMutex.Unlock()

	for rows.Next() {
		var report Report
		var logsString string

		if err := rows.Scan(&report.ID, &report.Channel.ID, &report.Channel.Name, &report.Channel.Type, &report.Reporter.ID, &report.Reporter.Name, &report.Target.ID, &report.Target.Name, &report.Reason, &logsString, &report.Time); err != nil {
			return err
		}

		report.Logs = strings.Split(logsString, "\n")

		h.reports[report.ID] = report
	}

	return nil
}

type handleReportMessage struct {
	Action    uint8
	ChannelID string
	ReportID  uint32
	Duration  *uint32
	Handler   ReportUser
}

func (h *Holder) Register(report Report) (*Report, bool, error) {
	const queryF = `
	INSERT INTO report
		(channel_id, channel_name, channel_type,
		reporter_id, reporter_name, target_id, target_name, reason, logs, time)
	VALUES
		($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
	RETURNING id
	`

	h.reportsMutex.Lock()
	defer h.reportsMutex.Unlock()

	// Don't accept reports for users that have already been reported
	for _, oldReport := range h.reports {
		if oldReport.Channel.ID == report.Channel.ID && oldReport.Target.ID == report.Target.ID {
			fmt.Println("Report already registered for this target in this channel")
			return &oldReport, false, nil
		}
	}

	row := h.db.QueryRow(queryF, report.Channel.ID, report.Channel.Name, report.Channel.Type, report.Reporter.ID, report.Reporter.Name, report.Target.ID, report.Target.Name, report.Reason, strings.Join(report.Logs, "\n"), report.Time) // GOOD
	var id int64
	err := row.Scan(&id)
	if err != nil {
		fmt.Printf("Error inserting report %v into SQL: %s\n", report, err)
		return nil, false, err
	}

	report.ID = uint32(id)

	h.pubSub.Publish(h, "ReportReceived", report)

	h.reports[report.ID] = report

	return &report, true, nil
}

func (h *Holder) Update(report Report) error {
	if report.ID == 0 {
		return errors.New("Missing report ID in Update")
	}

	const queryF = `UPDATE report SET time=$1, logs=$2 WHERE id=$3`
	_, err := h.db.Exec(queryF, report.Time, strings.Join(report.Logs, "\n"), report.ID) // GOOD
	if err != nil {
		return err
	}

	h.reportsMutex.Lock()
	defer h.reportsMutex.Unlock()
	h.reports[report.ID] = report

	// TODO: Send some "ReportUpdated" message

	return nil
}

type reportHandled struct {
	ReportID uint32
	Handler  ReportUser
	Action   uint8
}

func (h *Holder) insertHistoricReport(report Report, action handleReportMessage) {
	const queryF = `
INSERT INTO
	report_history
(
channel_id, channel_name, channel_type,
reporter_id, reporter_name,
target_id, target_name,
reason, logs,
time,
handler_id, handler_name,
action, action_duration,
time_handled
)

VALUES (
$1,$2,$3,
$4,$5,
$6,$7,
$8,$9,
$10,
$11,$12,
$13,$14,
$15
)`

	var actionDuration uint32
	if action.Duration != nil {
		actionDuration = *action.Duration
	}
	_, err := h.db.Exec(queryF, // GOOD
		report.Channel.ID, report.Channel.Name, report.Channel.Type,
		report.Reporter.ID, report.Reporter.Name,
		report.Target.ID, report.Target.Name,
		report.Reason, strings.Join(report.Logs, "\n"),
		report.Time,
		action.Handler.ID, action.Handler.Name,
		action.Action, actionDuration,
		time.Now())
	if err != nil {
		panic(err)
	}
}

func (h *Holder) handleReport(source pkg.PubSubSource, action handleReportMessage) error {
	h.reportsMutex.Lock()
	defer h.reportsMutex.Unlock()

	user := source.AuthenticatedUser()
	if user == nil {
		fmt.Println("Missing auth in HandleReport")
		return nil
	}

	report, ok := h.reports[action.ReportID]
	if !ok {
		fmt.Printf("No report found with ID %d\n", action.ReportID)
		// No report found with this ID
		return nil
	}

	if !user.HasPermission(report.Channel, pkg.PermissionModeration) {
		fmt.Println("user does not have moderation permission")
		return nil
	}

	// Remove report from SQL and our local map
	err := h.dismissReport(report.ID)
	if err != nil {
		fmt.Println("Error dismissing report", err)
		return err
	}

	action.Handler.Name = user.GetName()
	action.Handler.ID = user.GetID()

	bot := h.botStore.GetBotFromChannel(report.Channel.ID)
	reporterInst := bot.MakeUser(report.Reporter.Name)

	// TODO: Insert into new table: HandledReport
	h.insertHistoricReport(report, action)

	msg := &reportHandled{
		ReportID: report.ID,
		Handler: ReportUser{
			ID:   user.GetID(),
			Name: h.userStore.GetName(user.GetID()),
		},
		Action: action.Action,
	}

	h.pubSub.Publish(h, "ReportHandled", msg)

	switch action.Action {
	case pkg.ReportActionBan:
		h.pubSub.Publish(h, "Ban", &pkg.PubSubBan{
			Channel: report.Channel.Name,
			Target:  report.Target.Name,
			// Reason:  report.Reason,
		})
		bot.Whisper(reporterInst, fmt.Sprintf("Thanks to your report, user %s has been permanently banned", report.Target.Name))

	case pkg.ReportActionTimeout:
		var duration uint32
		duration = 600
		if action.Duration != nil {
			duration = *action.Duration
		}
		h.pubSub.Publish(h, "Timeout", &pkg.PubSubTimeout{
			Channel:  report.Channel.Name,
			Target:   report.Target.Name,
			Duration: duration,
			// Reason:   report.Reason,
		})
		bot.Whisper(reporterInst, fmt.Sprintf("Thanks to your report, user %s has been timed out for %d seconds", report.Target.Name, duration))

	case pkg.ReportActionDismiss:
		bot.Whisper(reporterInst, fmt.Sprintf("Your report of %s has been dismissed with no further action taken :\\", report.Target.Name))
		// We don't need to do anything else here, as we've already dismissed the report prior to the ban/timeout/untimeout events being sent out

	case pkg.ReportActionUndo:
		h.pubSub.Publish(h, "Untimeout", &pkg.PubSubUntimeout{
			Channel: report.Channel.Name,
			Target:  report.Target.Name,
		})
		bot.Whisper(reporterInst, fmt.Sprintf("Your report of %s has been undone with no further action taken :\\", report.Target.Name))

	default:
		fmt.Println("Unhandled action", action.Action)
	}

	return nil
}

// dismissReport assumes that reportsMutex has already been locked
func (h *Holder) dismissReport(reportID uint32) error {
	// Delete from SQL
	const queryF = "DELETE FROM report WHERE id=$1"

	_, err := h.db.Exec(queryF, reportID) // GOOD
	if err != nil {
		return err
	}

	// Delete from our internal storage
	delete(h.reports, reportID)

	return nil
}

func (h *Holder) handleBanEvent(banEvent pkg.PubSubBanEvent) error {
	h.reportsMutex.Lock()
	defer h.reportsMutex.Unlock()

	for reportID, report := range h.reports {
		if report.Channel.ID == banEvent.Channel.ID && report.Target.ID == banEvent.Target.ID {
			// Found matching report
			h.dismissReport(reportID)

			break
		}
	}

	return nil
}

func (h *Holder) MessageReceived(source pkg.PubSubSource, topic string, data []byte) error {
	switch topic {
	case "HandleReport":
		var msg handleReportMessage
		err := json.Unmarshal(data, &msg)
		if err != nil {
			fmt.Println("Error unmarshalling:", err)
			return nil
		}

		fmt.Printf("Handle report: %#v\n", msg)

		return h.handleReport(source, msg)

	case "BanEvent":
		var msg pkg.PubSubBanEvent
		err := json.Unmarshal(data, &msg)
		if err != nil {
			fmt.Println("Error unmarshalling:", err)
			return nil
		}

		return h.handleBanEvent(msg)
	}

	return nil
}

type reportReceivedParameters struct {
	ChannelID string
}

func (h *Holder) ConnectionSubscribed(source pkg.PubSubSource, topic string, parameters json.RawMessage) (error, bool) {
	switch topic {
	case "ReportReceived":
		fmt.Println("aaaaaaaaaaaaaaaa")
		user := source.AuthenticatedUser()
		if user == nil {
			fmt.Println("no user")
			return nil, false
		}

		fmt.Println("Parameters:", string(parameters))

		var parsedParams reportReceivedParameters
		err := json.Unmarshal(parameters, &parsedParams)
		if err != nil {
			fmt.Println("Error parsing subscription parameters:", err)
			return nil, false
		}

		channel := h.channelStore.TwitchChannel(parsedParams.ChannelID)
		if channel == nil {
			fmt.Println("Channel with id", parsedParams.ChannelID, "is not being moderated by us")
			return nil, false
		}

		fmt.Println("Channel ID:", parsedParams.ChannelID)

		fmt.Println("User name:", user.GetName())

		hasPermission := user.HasPermission(channel, pkg.PermissionModeration)

		if !hasPermission {
			fmt.Println("user", user.GetName(), "does not have permission in channel", channel.GetName())
			return nil, false
		}

		fmt.Println("Send reports to new connection")

		for _, report := range h.reports {
			bytes, err := json.Marshal(report)
			if err != nil {
				fmt.Println(err)
				return err, true
			}
			source.Connection().MessageReceived(h, topic, bytes)
		}
	}

	return nil, true
}
<commit_msg>Ensure we don't send reports for channels you didn't ask for (#67)<commit_after>package report

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"sync"
	"time"

	"github.com/pajbot/pajbot2/pkg"
)

type ReportUser struct {
	ID   string
	Name string
	Type string `json:",omitempty"`
}

func (u ReportUser) GetName() string {
	return u.Name
}

func (u ReportUser) GetID() string {
	return u.ID
}

type Report struct {
	ID       uint32
	Channel  ReportUser
	Reporter ReportUser
	Target   ReportUser
	Reason   string `json:",omitempty"`
	Logs     []string
	Time     time.Time
}

type HistoricReport struct {
	ID             uint32
	Channel        ReportUser
	Reporter       ReportUser
	Target         ReportUser
	Reason         string
	Logs           []string
	Time           time.Time
	Handler        ReportUser
	Action         uint8
	ActionDuration uint32
	TimeHandled    time.Time
}

type Holder struct {
	db           *sql.DB
	pubSub       pkg.PubSub
	userStore    pkg.UserStore
	channelStore pkg.ChannelStore
	botStore     pkg.BotStore

	reportsMutex *sync.Mutex
	reports      map[uint32]Report
}

var _ pkg.PubSubConnection = &Holder{}
var _ pkg.PubSubSource = &Holder{}
var _ pkg.PubSubSubscriptionHandler = &Holder{}

func New(app pkg.Application) (*Holder, error) {
	h := &Holder{
		db:           app.SQL(),
		pubSub:       app.PubSub(),
		userStore:    app.UserStore(),
		botStore:     app.TwitchBots(),
		channelStore: app.ChannelStore(),

		reportsMutex: &sync.Mutex{},
		reports:      make(map[uint32]Report),
	}

	err := h.Load()
	if err != nil {
		return nil, err
	}

	h.pubSub.Subscribe(h, "HandleReport", nil)
	h.pubSub.Subscribe(h, "TimeoutEvent", nil)
	h.pubSub.Subscribe(h, "BanEvent", nil)
	h.pubSub.HandleSubscribe(h, "ReportReceived")

	return h, nil
}

func (h *Holder) AuthenticatedUser() pkg.User {
	return nil
}

func (h *Holder) IsApplication() bool {
	return true
}

func (h *Holder) Connection() pkg.PubSubConnection {
	return h
}

func (h *Holder) Load() error {
	const queryF = `
SELECT
	id, channel_id, channel_name,
	channel_type, reporter_id, reporter_name,
	target_id, target_name,
	reason, logs, time
FROM
	report`

	rows, err := h.db.Query(queryF) // GOOD
	if err != nil {
		return err
	}
	defer rows.Close()

	h.reportsMutex.Lock()
	defer h.reportsMutex.Unlock()

	for rows.Next() {
		var report Report
		var logsString string

		if err := rows.Scan(&report.ID, &report.Channel.ID, &report.Channel.Name, &report.Channel.Type, &report.Reporter.ID, &report.Reporter.Name, &report.Target.ID, &report.Target.Name, &report.Reason, &logsString, &report.Time); err != nil {
			return err
		}

		report.Logs = strings.Split(logsString, "\n")

		h.reports[report.ID] = report
	}

	return nil
}

type handleReportMessage struct {
	Action    uint8
	ChannelID string
	ReportID  uint32
	Duration  *uint32
	Handler   ReportUser
}

func (h *Holder) Register(report Report) (*Report, bool, error) {
	const queryF = `
	INSERT INTO report
		(channel_id, channel_name, channel_type,
		reporter_id, reporter_name, target_id, target_name, reason, logs, time)
	VALUES
		($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
	RETURNING id
	`

	h.reportsMutex.Lock()
	defer h.reportsMutex.Unlock()

	// Don't accept reports for users that have already been reported
	for _, oldReport := range h.reports {
		if oldReport.Channel.ID == report.Channel.ID && oldReport.Target.ID == report.Target.ID {
			fmt.Println("Report already registered for this target in this channel")
			return &oldReport, false, nil
		}
	}

	row := h.db.QueryRow(queryF, report.Channel.ID, report.Channel.Name, report.Channel.Type, report.Reporter.ID, report.Reporter.Name, report.Target.ID, report.Target.Name, report.Reason, strings.Join(report.Logs, "\n"), report.Time) // GOOD
	var id int64
	err := row.Scan(&id)
	if err != nil {
		fmt.Printf("Error inserting report %v into SQL: %s\n", report, err)
		return nil, false, err
	}

	report.ID = uint32(id)

	h.pubSub.Publish(h, "ReportReceived", report)

	h.reports[report.ID] = report

	return &report, true, nil
}

func (h *Holder) Update(report Report) error {
	if report.ID == 0 {
		return errors.New("Missing report ID in Update")
	}

	const queryF = `UPDATE report SET time=$1, logs=$2 WHERE id=$3`
	_, err := h.db.Exec(queryF, report.Time, strings.Join(report.Logs, "\n"), report.ID) // GOOD
	if err != nil {
		return err
	}

	h.reportsMutex.Lock()
	defer h.reportsMutex.Unlock()
	h.reports[report.ID] = report

	// TODO: Send some "ReportUpdated" message

	return nil
}

type reportHandled struct {
	ReportID uint32
	Handler  ReportUser
	Action   uint8
}

func (h *Holder) insertHistoricReport(report Report, action handleReportMessage) {
	const queryF = `
INSERT INTO
	report_history
(
channel_id, channel_name, channel_type,
reporter_id, reporter_name,
target_id, target_name,
reason, logs,
time,
handler_id, handler_name,
action, action_duration,
time_handled
)

VALUES (
$1,$2,$3,
$4,$5,
$6,$7,
$8,$9,
$10,
$11,$12,
$13,$14,
$15
)`

	var actionDuration uint32
	if action.Duration != nil {
		actionDuration = *action.Duration
	}
	_, err := h.db.Exec(queryF, // GOOD
		report.Channel.ID, report.Channel.Name, report.Channel.Type,
		report.Reporter.ID, report.Reporter.Name,
		report.Target.ID, report.Target.Name,
		report.Reason, strings.Join(report.Logs, "\n"),
		report.Time,
		action.Handler.ID, action.Handler.Name,
		action.Action, actionDuration,
		time.Now())
	if err != nil {
		panic(err)
	}
}

func (h *Holder) handleReport(source pkg.PubSubSource, action handleReportMessage) error {
	h.reportsMutex.Lock()
	defer h.reportsMutex.Unlock()

	user := source.AuthenticatedUser()
	if user == nil {
		fmt.Println("Missing auth in HandleReport")
		return nil
	}

	report, ok := h.reports[action.ReportID]
	if !ok {
		fmt.Printf("No report found with ID %d\n", action.ReportID)
		// No report found with this ID
		return nil
	}

	if !user.HasPermission(report.Channel, pkg.PermissionModeration) {
		fmt.Println("user does not have moderation permission")
		return nil
	}

	// Remove report from SQL and our local map
	err := h.dismissReport(report.ID)
	if err != nil {
		fmt.Println("Error dismissing report", err)
		return err
	}

	action.Handler.Name = user.GetName()
	action.Handler.ID = user.GetID()

	bot := h.botStore.GetBotFromChannel(report.Channel.ID)
	reporterInst := bot.MakeUser(report.Reporter.Name)

	// TODO: Insert into new table: HandledReport
	h.insertHistoricReport(report, action)

	msg := &reportHandled{
		ReportID: report.ID,
		Handler: ReportUser{
			ID:   user.GetID(),
			Name: h.userStore.GetName(user.GetID()),
		},
		Action: action.Action,
	}

	h.pubSub.Publish(h, "ReportHandled", msg)

	switch action.Action {
	case pkg.ReportActionBan:
		h.pubSub.Publish(h, "Ban", &pkg.PubSubBan{
			Channel: report.Channel.Name,
			Target:  report.Target.Name,
			// Reason:  report.Reason,
		})
		bot.Whisper(reporterInst, fmt.Sprintf("Thanks to your report, user %s has been permanently banned", report.Target.Name))

	case pkg.ReportActionTimeout:
		var duration uint32
		duration = 600
		if action.Duration != nil {
			duration = *action.Duration
		}
		h.pubSub.Publish(h, "Timeout", &pkg.PubSubTimeout{
			Channel:  report.Channel.Name,
			Target:   report.Target.Name,
			Duration: duration,
			// Reason:   report.Reason,
		})
		bot.Whisper(reporterInst, fmt.Sprintf("Thanks to your report, user %s has been timed out for %d seconds", report.Target.Name, duration))

	case pkg.ReportActionDismiss:
		bot.Whisper(reporterInst, fmt.Sprintf("Your report of %s has been dismissed with no further action taken :\\", report.Target.Name))
		// We don't need to do anything else here, as we've already dismissed the report prior to the ban/timeout/untimeout events being sent out

	case pkg.ReportActionUndo:
		h.pubSub.Publish(h, "Untimeout", &pkg.PubSubUntimeout{
			Channel: report.Channel.Name,
			Target:  report.Target.Name,
		})
		bot.Whisper(reporterInst, fmt.Sprintf("Your report of %s has been undone with no further action taken :\\", report.Target.Name))

	default:
		fmt.Println("Unhandled action", action.Action)
	}

	return nil
}

// dismissReport assumes that reportsMutex has already been locked
func (h *Holder) dismissReport(reportID uint32) error {
	// Delete from SQL
	const queryF = "DELETE FROM report WHERE id=$1"

	_, err := h.db.Exec(queryF, reportID) // GOOD
	if err != nil {
		return err
	}

	// Delete from our internal storage
	delete(h.reports, reportID)

	return nil
}

func (h *Holder) handleBanEvent(banEvent pkg.PubSubBanEvent) error {
	h.reportsMutex.Lock()
	defer h.reportsMutex.Unlock()

	for reportID, report := range h.reports {
		if report.Channel.ID == banEvent.Channel.ID && report.Target.ID == banEvent.Target.ID {
			// Found matching report
			h.dismissReport(reportID)

			break
		}
	}

	return nil
}

func (h *Holder) MessageReceived(source pkg.PubSubSource, topic string, data []byte) error {
	switch topic {
	case "HandleReport":
		var msg handleReportMessage
		err := json.Unmarshal(data, &msg)
		if err != nil {
			fmt.Println("Error unmarshalling:", err)
			return nil
		}

		fmt.Printf("Handle report: %#v\n", msg)

		return h.handleReport(source, msg)

	case "BanEvent":
		var msg pkg.PubSubBanEvent
		err := json.Unmarshal(data, &msg)
		if err != nil {
			fmt.Println("Error unmarshalling:", err)
			return nil
		}

		return h.handleBanEvent(msg)
	}

	return nil
}

type reportReceivedParameters struct {
	ChannelID string
}

func (h *Holder) ConnectionSubscribed(source pkg.PubSubSource, topic string, parameters json.RawMessage) (error, bool) {
	switch topic {
	case "ReportReceived":
		fmt.Println("aaaaaaaaaaaaaaaa")
		user := source.AuthenticatedUser()
		if user == nil {
			fmt.Println("no user")
			return nil, false
		}

		fmt.Println("Parameters:", string(parameters))

		var parsedParams reportReceivedParameters
		err := json.Unmarshal(parameters, &parsedParams)
		if err != nil {
			fmt.Println("Error parsing subscription parameters:", err)
			return nil, false
		}

		channel := h.channelStore.TwitchChannel(parsedParams.ChannelID)
		if channel == nil {
			fmt.Println("Channel with id", parsedParams.ChannelID, "is not being moderated by us")
			return nil, false
		}

		fmt.Println("Channel ID:", parsedParams.ChannelID)

		fmt.Println("User name:", user.GetName())

		hasPermission := user.HasPermission(channel, pkg.PermissionModeration)

		if !hasPermission {
			fmt.Println("user", user.GetName(), "does not have permission in channel", channel.GetName())
			return nil, false
		}

		fmt.Println("Send reports to new connection")

		for _, report := range h.reports {
			if report.Channel.GetID() != channel.GetID() {
				continue
			}
			bytes, err := json.Marshal(report)
			if err != nil {
				fmt.Println(err)
				return err, true
			}
			source.Connection().MessageReceived(h, topic, bytes)
		}
	}

	return nil, true
}
<|endoftext|>