Merge pull request #10350 from spowelljr/addAudit
Add audit logs to minikube logs outputpull/10527/head
commit
26351ff6c6
|
@ -25,6 +25,7 @@ import (
|
|||
"github.com/spf13/viper"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/minikube/pkg/minikube/config"
|
||||
"k8s.io/minikube/pkg/version"
|
||||
)
|
||||
|
||||
// userName pulls the user flag, if empty gets the os username.
|
||||
|
@ -54,8 +55,8 @@ func Log(startTime time.Time) {
|
|||
if !shouldLog() {
|
||||
return
|
||||
}
|
||||
e := newEntry(os.Args[1], args(), userName(), startTime, time.Now())
|
||||
if err := appendToLog(e); err != nil {
|
||||
r := newRow(os.Args[1], args(), userName(), version.GetVersion(), startTime, time.Now())
|
||||
if err := appendToLog(r); err != nil {
|
||||
klog.Error(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package audit
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/minikube/pkg/minikube/config"
|
||||
"k8s.io/minikube/pkg/minikube/constants"
|
||||
)
|
||||
|
||||
// entry represents the execution of a command.
|
||||
type entry struct {
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
// Type returns the cloud events compatible type of this struct.
|
||||
func (e *entry) Type() string {
|
||||
return "io.k8s.sigs.minikube.audit"
|
||||
}
|
||||
|
||||
// newEntry returns a new audit type.
|
||||
func newEntry(command string, args string, user string, startTime time.Time, endTime time.Time) *entry {
|
||||
return &entry{
|
||||
map[string]string{
|
||||
"args": args,
|
||||
"command": command,
|
||||
"endTime": endTime.Format(constants.TimeFormat),
|
||||
"profile": viper.GetString(config.ProfileName),
|
||||
"startTime": startTime.Format(constants.TimeFormat),
|
||||
"user": user,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -30,7 +30,7 @@ var currentLogFile *os.File
|
|||
// setLogFile sets the logPath and creates the log file if it doesn't exist.
|
||||
func setLogFile() error {
|
||||
lp := localpath.AuditLog()
|
||||
f, err := os.OpenFile(lp, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
f, err := os.OpenFile(lp, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open %s: %v", lp, err)
|
||||
}
|
||||
|
@ -38,15 +38,15 @@ func setLogFile() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// appendToLog appends the audit entry to the log file.
|
||||
func appendToLog(entry *entry) error {
|
||||
// appendToLog appends the row to the log file.
|
||||
func appendToLog(row *row) error {
|
||||
if currentLogFile == nil {
|
||||
if err := setLogFile(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
e := register.CloudEvent(entry, entry.data)
|
||||
bs, err := e.MarshalJSON()
|
||||
ce := register.CloudEvent(row, row.toMap())
|
||||
bs, err := ce.MarshalJSON()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error marshalling event: %v", err)
|
||||
}
|
||||
|
|
|
@ -42,8 +42,8 @@ func TestLogFile(t *testing.T) {
|
|||
defer func() { currentLogFile = &oldLogFile }()
|
||||
currentLogFile = f
|
||||
|
||||
e := newEntry("start", "-v", "user1", time.Now(), time.Now())
|
||||
if err := appendToLog(e); err != nil {
|
||||
r := newRow("start", "-v", "user1", "v0.17.1", time.Now(), time.Now())
|
||||
if err := appendToLog(r); err != nil {
|
||||
t.Fatalf("Error appendingToLog: %v", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
Copyright 2020 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package audit
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// RawReport contains the information required to generate formatted reports.
|
||||
type RawReport struct {
|
||||
headers []string
|
||||
rows []row
|
||||
}
|
||||
|
||||
// Report is created using the last n lines from the log file.
|
||||
func Report(lastNLines int) (*RawReport, error) {
|
||||
if lastNLines <= 0 {
|
||||
return nil, fmt.Errorf("last n lines must be 1 or greater")
|
||||
}
|
||||
if currentLogFile == nil {
|
||||
if err := setLogFile(); err != nil {
|
||||
return nil, fmt.Errorf("failed to set the log file: %v", err)
|
||||
}
|
||||
}
|
||||
var logs []string
|
||||
s := bufio.NewScanner(currentLogFile)
|
||||
for s.Scan() {
|
||||
// pop off the earliest line if already at desired log length
|
||||
if len(logs) == lastNLines {
|
||||
logs = logs[1:]
|
||||
}
|
||||
logs = append(logs, s.Text())
|
||||
}
|
||||
if err := s.Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read from audit file: %v", err)
|
||||
}
|
||||
rows, err := logsToRows(logs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert logs to rows: %v", err)
|
||||
}
|
||||
r := &RawReport{
|
||||
[]string{"Command", "Args", "Profile", "User", "Version", "Start Time", "End Time"},
|
||||
rows,
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// ASCIITable creates a formatted table using the headers and rows from the report.
|
||||
func (rr *RawReport) ASCIITable() string {
|
||||
return rowsToASCIITable(rr.rows, rr.headers)
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
Copyright 2020 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package audit
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReport(t *testing.T) {
|
||||
f, err := ioutil.TempFile("", "audit.json")
|
||||
if err != nil {
|
||||
t.Fatalf("failed creating temporary file: %v", err)
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
|
||||
s := `{"data":{"args":"-p mini1","command":"start","endTime":"Wed, 03 Feb 2021 15:33:05 MST","profile":"mini1","startTime":"Wed, 03 Feb 2021 15:30:33 MST","user":"user1"},"datacontenttype":"application/json","id":"9b7593cb-fbec-49e5-a3ce-bdc2d0bfb208","source":"https://minikube.sigs.k8s.io/","specversion":"1.0","type":"io.k8s.si gs.minikube.audit"}
|
||||
{"data":{"args":"-p mini1","command":"start","endTime":"Wed, 03 Feb 2021 15:33:05 MST","profile":"mini1","startTime":"Wed, 03 Feb 2021 15:30:33 MST","user":"user1"},"datacontenttype":"application/json","id":"9b7593cb-fbec-49e5-a3ce-bdc2d0bfb208","source":"https://minikube.sigs.k8s.io/","specversion":"1.0","type":"io.k8s.si gs.minikube.audit"}
|
||||
{"data":{"args":"--user user2","command":"logs","endTime":"Tue, 02 Feb 2021 16:46:20 MST","profile":"minikube","startTime":"Tue, 02 Feb 2021 16:46:00 MST","user":"user2"},"datacontenttype":"application/json","id":"fec03227-2484-48b6-880a-88fd010b5efd","source":"https://minikube.sigs.k8s.io/","specversion":"1.0","type":"io.k8s.sigs.minikube.audit"}`
|
||||
|
||||
if _, err := f.WriteString(s); err != nil {
|
||||
t.Fatalf("failed writing to file: %v", err)
|
||||
}
|
||||
if _, err := f.Seek(0, 0); err != nil {
|
||||
t.Fatalf("failed seeking to start of file: %v", err)
|
||||
}
|
||||
|
||||
oldLogFile := *currentLogFile
|
||||
defer func() { currentLogFile = &oldLogFile }()
|
||||
currentLogFile = f
|
||||
|
||||
wantedLines := 2
|
||||
r, err := Report(wantedLines)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create report: %v", err)
|
||||
}
|
||||
|
||||
if len(r.rows) != wantedLines {
|
||||
t.Errorf("report has %d lines of logs, want %d", len(r.rows), wantedLines)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
Copyright 2020 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package audit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/minikube/pkg/minikube/config"
|
||||
"k8s.io/minikube/pkg/minikube/constants"
|
||||
)
|
||||
|
||||
// row is the log of a single command.
|
||||
type row struct {
|
||||
args string
|
||||
command string
|
||||
endTime string
|
||||
profile string
|
||||
startTime string
|
||||
user string
|
||||
version string
|
||||
Data map[string]string `json:"data"`
|
||||
}
|
||||
|
||||
// Type returns the cloud events compatible type of this struct.
|
||||
func (e *row) Type() string {
|
||||
return "io.k8s.sigs.minikube.audit"
|
||||
}
|
||||
|
||||
// assignFields converts the map values to their proper fields,
|
||||
// to be used when converting from JSON Cloud Event format.
|
||||
func (e *row) assignFields() {
|
||||
e.args = e.Data["args"]
|
||||
e.command = e.Data["command"]
|
||||
e.endTime = e.Data["endTime"]
|
||||
e.profile = e.Data["profile"]
|
||||
e.startTime = e.Data["startTime"]
|
||||
e.user = e.Data["user"]
|
||||
e.version = e.Data["version"]
|
||||
}
|
||||
|
||||
// toMap combines fields into a string map,
|
||||
// to be used when converting to JSON Cloud Event format.
|
||||
func (e *row) toMap() map[string]string {
|
||||
return map[string]string{
|
||||
"args": e.args,
|
||||
"command": e.command,
|
||||
"endTime": e.endTime,
|
||||
"profile": e.profile,
|
||||
"startTime": e.startTime,
|
||||
"user": e.user,
|
||||
"version": e.version,
|
||||
}
|
||||
}
|
||||
|
||||
// newRow creates a new audit row.
|
||||
func newRow(command string, args string, user string, version string, startTime time.Time, endTime time.Time, profile ...string) *row {
|
||||
p := viper.GetString(config.ProfileName)
|
||||
if len(profile) > 0 {
|
||||
p = profile[0]
|
||||
}
|
||||
return &row{
|
||||
args: args,
|
||||
command: command,
|
||||
endTime: endTime.Format(constants.TimeFormat),
|
||||
profile: p,
|
||||
startTime: startTime.Format(constants.TimeFormat),
|
||||
user: user,
|
||||
version: version,
|
||||
}
|
||||
}
|
||||
|
||||
// toFields converts a row to an array of fields,
|
||||
// to be used when converting to a table.
|
||||
func (e *row) toFields() []string {
|
||||
return []string{e.command, e.args, e.profile, e.user, e.version, e.startTime, e.endTime}
|
||||
}
|
||||
|
||||
// logsToRows converts audit logs into arrays of rows.
|
||||
func logsToRows(logs []string) ([]row, error) {
|
||||
rows := []row{}
|
||||
for _, l := range logs {
|
||||
r := row{}
|
||||
if err := json.Unmarshal([]byte(l), &r); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal %q: %v", l, err)
|
||||
}
|
||||
r.assignFields()
|
||||
rows = append(rows, r)
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
// rowsToASCIITable converts rows into a formatted ASCII table.
|
||||
func rowsToASCIITable(rows []row, headers []string) string {
|
||||
c := [][]string{}
|
||||
for _, r := range rows {
|
||||
c = append(c, r.toFields())
|
||||
}
|
||||
b := new(bytes.Buffer)
|
||||
t := tablewriter.NewWriter(b)
|
||||
t.SetHeader(headers)
|
||||
t.SetAutoFormatHeaders(false)
|
||||
t.SetBorders(tablewriter.Border{Left: true, Top: true, Right: true, Bottom: true})
|
||||
t.SetCenterSeparator("|")
|
||||
t.AppendBulk(c)
|
||||
t.Render()
|
||||
return b.String()
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
Copyright 2020 The Kubernetes Authors All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package audit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/minikube/pkg/minikube/constants"
|
||||
)
|
||||
|
||||
func TestRow(t *testing.T) {
|
||||
c := "start"
|
||||
a := "--alsologtostderr"
|
||||
p := "profile1"
|
||||
u := "user1"
|
||||
v := "v0.17.1"
|
||||
st := time.Now()
|
||||
stFormatted := st.Format(constants.TimeFormat)
|
||||
et := time.Now()
|
||||
etFormatted := et.Format(constants.TimeFormat)
|
||||
|
||||
r := newRow(c, a, u, v, st, et, p)
|
||||
|
||||
t.Run("NewRow", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
key string
|
||||
got string
|
||||
want string
|
||||
}{
|
||||
{"command", r.command, c},
|
||||
{"args", r.args, a},
|
||||
{"profile", r.profile, p},
|
||||
{"user", r.user, u},
|
||||
{"version", r.version, v},
|
||||
{"startTime", r.startTime, stFormatted},
|
||||
{"endTime", r.endTime, etFormatted},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if tt.got != tt.want {
|
||||
t.Errorf("row.%s = %s; want %s", tt.key, tt.got, tt.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Type", func(t *testing.T) {
|
||||
got := r.Type()
|
||||
want := "io.k8s.sigs.minikube.audit"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("Type() = %s; want %s", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("toMap", func(t *testing.T) {
|
||||
m := r.toMap()
|
||||
|
||||
tests := []struct {
|
||||
key string
|
||||
want string
|
||||
}{
|
||||
{"command", c},
|
||||
{"args", a},
|
||||
{"profile", p},
|
||||
{"user", u},
|
||||
{"version", v},
|
||||
{"startTime", stFormatted},
|
||||
{"endTime", etFormatted},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got := m[tt.key]
|
||||
if got != tt.want {
|
||||
t.Errorf("map[%s] = %s; want %s", tt.key, got, tt.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("toFields", func(t *testing.T) {
|
||||
got := r.toFields()
|
||||
gotString := strings.Join(got, ",")
|
||||
want := []string{c, a, p, u, v, stFormatted, etFormatted}
|
||||
wantString := strings.Join(want, ",")
|
||||
|
||||
if gotString != wantString {
|
||||
t.Errorf("toFields() = %s; want %s", gotString, wantString)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("assignFields", func(t *testing.T) {
|
||||
l := fmt.Sprintf(`{"data":{"args":"%s","command":"%s","endTime":"%s","profile":"%s","startTime":"%s","user":"%s","version":"v0.17.1"},"datacontenttype":"application/json","id":"bc6ec9d4-0d08-4b57-ac3b-db8d67774768","source":"https://minikube.sigs.k8s.io/","specversion":"1.0","type":"io.k8s.sigs.minikube.audit"}`, a, c, etFormatted, p, stFormatted, u)
|
||||
|
||||
r := &row{}
|
||||
if err := json.Unmarshal([]byte(l), r); err != nil {
|
||||
t.Fatalf("failed to unmarshal log: %v", err)
|
||||
}
|
||||
|
||||
r.assignFields()
|
||||
|
||||
tests := []struct {
|
||||
key string
|
||||
got string
|
||||
want string
|
||||
}{
|
||||
{"command", r.command, c},
|
||||
{"args", r.args, a},
|
||||
{"profile", r.profile, p},
|
||||
{"user", r.user, u},
|
||||
{"version", r.version, v},
|
||||
{"startTime", r.startTime, stFormatted},
|
||||
{"endTime", r.endTime, etFormatted},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if tt.got != tt.want {
|
||||
t.Errorf("singleEntry.%s = %s; want %s", tt.key, tt.got, tt.want)
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -29,6 +29,7 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/minikube/pkg/minikube/audit"
|
||||
"k8s.io/minikube/pkg/minikube/bootstrapper"
|
||||
"k8s.io/minikube/pkg/minikube/command"
|
||||
"k8s.io/minikube/pkg/minikube/config"
|
||||
|
@ -188,12 +189,29 @@ func Output(r cruntime.Manager, bs bootstrapper.Bootstrapper, cfg config.Cluster
|
|||
}
|
||||
}
|
||||
|
||||
if err := outputAudit(lines); err != nil {
|
||||
klog.Error(err)
|
||||
failed = append(failed, "audit")
|
||||
}
|
||||
|
||||
if len(failed) > 0 {
|
||||
return fmt.Errorf("unable to fetch logs for: %s", strings.Join(failed, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// outputAudit displays the audit logs.
|
||||
func outputAudit(lines int) error {
|
||||
out.Step(style.Empty, "")
|
||||
out.Step(style.Empty, "==> Audit <==")
|
||||
r, err := audit.Report(lines)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create audit report: %v", err)
|
||||
}
|
||||
out.Step(style.Empty, r.ASCIITable())
|
||||
return nil
|
||||
}
|
||||
|
||||
// logCommands returns a list of commands that would be run to receive the anticipated logs
|
||||
func logCommands(r cruntime.Manager, bs bootstrapper.Bootstrapper, cfg config.ClusterConfig, length int, follow bool) map[string]string {
|
||||
cmds := bs.LogCommands(cfg, bootstrapper.LogOptions{Lines: length, Follow: follow})
|
||||
|
|
|
@ -797,7 +797,7 @@ func validateLogsCmd(ctx context.Context, t *testing.T, profile string) {
|
|||
if err != nil {
|
||||
t.Errorf("%s failed: %v", rr.Command(), err)
|
||||
}
|
||||
expectedWords := []string{"apiserver", "Linux", "kubelet"}
|
||||
expectedWords := []string{"apiserver", "Linux", "kubelet", "Audit"}
|
||||
switch ContainerRuntime() {
|
||||
case "docker":
|
||||
expectedWords = append(expectedWords, "Docker")
|
||||
|
|
|
@ -196,6 +196,14 @@ func TestStoppedBinaryUpgrade(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("upgrade from %s to HEAD failed: %s: %v", legacyVersion, rr.Command(), err)
|
||||
}
|
||||
|
||||
t.Run("MinikubeLogs", func(t *testing.T) {
|
||||
args := []string{"logs", "-p", profile}
|
||||
rr, err = Run(t, exec.CommandContext(ctx, Target(), args...))
|
||||
if err != nil {
|
||||
t.Fatalf("`minikube logs` after upgrade to HEAD from %s failed: %v", legacyVersion, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestKubernetesUpgrade upgrades Kubernetes from oldest to newest
|
||||
|
|
Loading…
Reference in New Issue