Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

poc: a metrics module for pebble #519

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
9 changes: 8 additions & 1 deletion client/identities.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ type Identity struct {
Access IdentityAccess `json:"access" yaml:"access"`

// One or more of the following type-specific configuration fields must be
// non-nil (currently the only type is "local").
// non-nil (currently the only types are "local" and "basic").
Local *LocalIdentity `json:"local,omitempty" yaml:"local,omitempty"`
Basic *BasicIdentity `json:"basic,omitempty" yaml:"basic,omitempty"`
}

// IdentityAccess defines the access level for an identity.
Expand All @@ -47,6 +48,12 @@ type LocalIdentity struct {
UserID *uint32 `json:"user-id" yaml:"user-id"`
}

// BasicIdentity holds identity configuration specific to the "basic" type
// (for username/password authentication).
type BasicIdentity struct {
Password string `json:"password" yaml:"password"`
}

// For future extension.
type IdentitiesOptions struct{}

Expand Down
3 changes: 3 additions & 0 deletions internals/cli/cmd_identities.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ func (cmd *cmdIdentities) writeText(identities map[string]*client.Identity) erro
if identity.Local != nil {
types = append(types, "local")
}
if identity.Basic != nil {
types = append(types, "basic")
}
sort.Strings(types)
if len(types) == 0 {
types = append(types, "unknown")
Expand Down
19 changes: 17 additions & 2 deletions internals/daemon/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (ac AdminAccess) CheckAccess(d *Daemon, r *http.Request, user *UserState) R
if user == nil {
return Unauthorized(accessDenied)
}
if user.Access == state.AdminAccess {
if user.Identity.Access == state.AdminAccess {
return nil
}
// An identity explicitly set to "access: read" or "access: untrusted" isn't allowed.
Expand All @@ -61,10 +61,25 @@ func (ac UserAccess) CheckAccess(d *Daemon, r *http.Request, user *UserState) Re
if user == nil {
return Unauthorized(accessDenied)
}
switch user.Access {
switch user.Identity.Access {
case state.ReadAccess, state.AdminAccess:
return nil
}
// An identity explicitly set to "access: untrusted" isn't allowed.
return Unauthorized(accessDenied)
}

// MetricsAccess allows requests over the UNIX domain socket from any local user
type MetricsAccess struct{}

func (ac MetricsAccess) CheckAccess(d *Daemon, r *http.Request, user *UserState) Response {
if user == nil {
return Unauthorized(accessDenied)
}
switch user.Identity.Access {
case state.MetricsAccess, state.AdminAccess:
return nil
}
// An identity explicitly set to "access: untrusted" isn't allowed.
return Unauthorized(accessDenied)
}
4 changes: 4 additions & 0 deletions internals/daemon/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ var API = []*Command{{
WriteAccess: AdminAccess{},
GET: v1GetIdentities,
POST: v1PostIdentities,
}, {
Path: "/metrics",
ReadAccess: MetricsAccess{},
GET: Metrics,
}}

var (
Expand Down
34 changes: 34 additions & 0 deletions internals/daemon/api_metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) 2024 Canonical Ltd
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 3 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package daemon

import (
"net/http"

"github.com/canonical/pebble/internals/metrics"
)

func Metrics(c *Command, r *http.Request, _ *UserState) Response {
return metricsResponse{}
}

// metricsResponse is a Response implementation to serve the metrics in a prometheus metrics format.
type metricsResponse struct{}

func (r metricsResponse) ServeHTTP(w http.ResponseWriter, req *http.Request) {
registry := metrics.GetRegistry()
w.WriteHeader(http.StatusOK)
w.Write([]byte(registry.GatherMetrics()))
}
18 changes: 9 additions & 9 deletions internals/daemon/api_notices.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,16 @@ type addedNotice struct {
func v1GetNotices(c *Command, r *http.Request, user *UserState) Response {
// TODO(benhoyt): the design of notices presumes UIDs; if in future when we
// support identities that aren't UID based, we'll need to fix this.
if user == nil || user.UID == nil {
if user == nil || user.Identity.Local == nil {
return Forbidden("cannot determine UID of request, so cannot retrieve notices")
}

// By default, return notices with the request UID and public notices.
userID := user.UID
userID := &user.Identity.Local.UserID

query := r.URL.Query()
if len(query["user-id"]) > 0 {
if user.Access != state.AdminAccess {
if user.Identity.Access != state.AdminAccess {
return Forbidden(`only admins may use the "user-id" filter`)
}
var err error
Expand All @@ -65,7 +65,7 @@ func v1GetNotices(c *Command, r *http.Request, user *UserState) Response {
}

if len(query["users"]) > 0 {
if user.Access != state.AdminAccess {
if user.Identity.Access != state.AdminAccess {
return Forbidden(`only admins may use the "users" filter`)
}
if len(query["user-id"]) > 0 {
Expand Down Expand Up @@ -179,7 +179,7 @@ func sanitizeTypesFilter(queryTypes []string) ([]state.NoticeType, error) {
}

func v1PostNotices(c *Command, r *http.Request, user *UserState) Response {
if user == nil || user.UID == nil {
if user == nil || user.Identity.Local == nil {
return Forbidden("cannot determine UID of request, so cannot create notice")
}

Expand Down Expand Up @@ -228,7 +228,7 @@ func v1PostNotices(c *Command, r *http.Request, user *UserState) Response {
st.Lock()
defer st.Unlock()

noticeId, err := st.AddNotice(user.UID, state.CustomNotice, payload.Key, &state.AddNoticeOptions{
noticeId, err := st.AddNotice(&user.Identity.Local.UserID, state.CustomNotice, payload.Key, &state.AddNoticeOptions{
Data: data,
RepeatAfter: repeatAfter,
})
Expand All @@ -240,7 +240,7 @@ func v1PostNotices(c *Command, r *http.Request, user *UserState) Response {
}

func v1GetNotice(c *Command, r *http.Request, user *UserState) Response {
if user == nil || user.UID == nil {
if user == nil || user.Identity.Local == nil {
return Forbidden("cannot determine UID of request, so cannot retrieve notice")
}
noticeID := muxVars(r)["id"]
Expand All @@ -263,10 +263,10 @@ func noticeViewableByUser(notice *state.Notice, user *UserState) bool {
// Notice has no UID, so it's viewable by any user (with a UID).
return true
}
if user.Access == state.AdminAccess {
if user.Identity.Access == state.AdminAccess {
// User is admin, they can view anything.
return true
}
// Otherwise user's UID must match notice's UID.
return *user.UID == userID
return user.Identity.Local.UserID == userID
}
59 changes: 47 additions & 12 deletions internals/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"errors"
"fmt"
"io"
"math/rand"
"net"
"net/http"
"os"
Expand All @@ -36,6 +37,7 @@ import (
"gopkg.in/tomb.v2"

"github.com/canonical/pebble/internals/logger"
"github.com/canonical/pebble/internals/metrics"
"github.com/canonical/pebble/internals/osutil"
"github.com/canonical/pebble/internals/osutil/sys"
"github.com/canonical/pebble/internals/overlord"
Expand Down Expand Up @@ -115,8 +117,9 @@ type Daemon struct {

// UserState represents the state of an authenticated API user.
type UserState struct {
Access state.IdentityAccess
UID *uint32
// Access state.IdentityAccess
// UID *uint32
Identity *state.Identity
}

// A ResponseFunc handles one of the individual verbs for a method
Expand Down Expand Up @@ -146,22 +149,21 @@ const (
accessForbidden
)

func userFromRequest(st *state.State, r *http.Request, ucred *Ucrednet) (*UserState, error) {
if ucred == nil {
// No ucred details, no UserState. Currently, "local" (ucred-based) is
// the only type of identity we support.
return nil, nil
func userFromRequest(st *state.State, r *http.Request, ucred *Ucrednet, username, password string) (*UserState, error) {
var userID *uint32
if ucred != nil {
userID = &ucred.Uid
}

st.Lock()
identity := st.IdentityFromInputs(&ucred.Uid)
identity := st.IdentityFromInputs(userID, username, password)
st.Unlock()

if identity == nil {
// No identity that matches these inputs (for now, just UID).
return nil, nil
}
return &UserState{Access: identity.Access, UID: &ucred.Uid}, nil
return &UserState{Identity: identity}, nil
}

func (d *Daemon) Overlord() *overlord.Overlord {
Expand Down Expand Up @@ -212,7 +214,8 @@ func (c *Command) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// not good: https://github.com/canonical/pebble/pull/369
var user *UserState
if _, isOpen := access.(OpenAccess); !isOpen {
user, err = userFromRequest(c.d.state, r, ucred)
basicAuthUsername, basicAuthPassword, _ := r.BasicAuth()
user, err = userFromRequest(c.d.state, r, ucred, basicAuthUsername, basicAuthPassword)
if err != nil {
Forbidden("forbidden").ServeHTTP(w, r)
return
Expand All @@ -223,10 +226,26 @@ func (c *Command) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if user == nil && ucred != nil {
if ucred.Uid == 0 || ucred.Uid == uint32(os.Getuid()) {
// Admin if UID is 0 (root) or the UID the daemon is running as.
user = &UserState{Access: state.AdminAccess, UID: &ucred.Uid}
// user = &UserState{Access: state.AdminAccess, UID: &ucred.Uid}
user = &UserState{
Identity: &state.Identity{
Access: state.AdminAccess,
Local: &state.LocalIdentity{
UserID: ucred.Uid,
},
},
}
} else {
// Regular read access if any other local UID.
user = &UserState{Access: state.ReadAccess, UID: &ucred.Uid}
// user = &UserState{Access: state.ReadAccess, UID: &ucred.Uid}
user = &UserState{
Identity: &state.Identity{
Access: state.ReadAccess,
Local: &state.LocalIdentity{
UserID: ucred.Uid,
},
},
}
}
}

Expand Down Expand Up @@ -366,6 +385,22 @@ func (d *Daemon) Init() error {
}

logger.Noticef("Started daemon.")

registry := metrics.GetRegistry()
myCounter := registry.NewCounterVec("my_counter", "Total number of something processed.", []string{"operation", "status"})
myGauge := registry.NewGaugeVec("my_gauge", "Current value of something.", []string{"sensor"})
// Goroutine to update metrics randomly
go func() {
for {
myCounter.WithLabelValues("read", "success").Inc()
myCounter.WithLabelValues("write", "success").Add(2)
myCounter.WithLabelValues("read", "failed").Inc()
myGauge.WithLabelValues("temperature").Set(20.0 + rand.Float64()*10.0)

time.Sleep(time.Duration(rand.Intn(5)+1) * time.Second) // Random sleep between 1 and 5 seconds
}
}()

return nil
}

Expand Down
Loading
Loading