2021-08-12 17:13:11 -04:00
//go:build linux
2020-02-26 16:59:22 -05:00
// +build linux
/ *
Copyright 2020 The Kubernetes Authors .
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
package volume
import (
2020-02-28 12:23:31 -05:00
"fmt"
2020-02-26 16:59:22 -05:00
"os"
2020-02-28 12:23:31 -05:00
"path/filepath"
2020-02-26 16:59:22 -05:00
"syscall"
"testing"
2025-02-26 13:27:32 -05:00
"time"
2020-02-26 16:59:22 -05:00
v1 "k8s.io/api/core/v1"
2025-02-26 13:27:32 -05:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/record"
2020-02-26 16:59:22 -05:00
utiltesting "k8s.io/client-go/util/testing"
)
type localFakeMounter struct {
path string
attributes Attributes
}
func ( l * localFakeMounter ) GetPath ( ) string {
return l . path
}
func ( l * localFakeMounter ) GetAttributes ( ) Attributes {
return l . attributes
}
func ( l * localFakeMounter ) SetUp ( mounterArgs MounterArgs ) error {
return nil
}
func ( l * localFakeMounter ) SetUpAt ( dir string , mounterArgs MounterArgs ) error {
return nil
}
func ( l * localFakeMounter ) GetMetrics ( ) ( * Metrics , error ) {
return nil , nil
}
func TestSkipPermissionChange ( t * testing . T ) {
2020-02-28 12:23:31 -05:00
always := v1 . FSGroupChangeAlways
onrootMismatch := v1 . FSGroupChangeOnRootMismatch
2020-02-26 16:59:22 -05:00
tests := [ ] struct {
description string
fsGroupChangePolicy * v1 . PodFSGroupChangePolicy
gidOwnerMatch bool
permissionMatch bool
sgidMatch bool
skipPermssion bool
} {
{
description : "skippermission=false, policy=nil" ,
skipPermssion : false ,
} ,
{
description : "skippermission=false, policy=always" ,
fsGroupChangePolicy : & always ,
skipPermssion : false ,
} ,
2020-02-28 12:23:31 -05:00
{
description : "skippermission=false, policy=always, gidmatch=true" ,
fsGroupChangePolicy : & always ,
skipPermssion : false ,
gidOwnerMatch : true ,
} ,
{
description : "skippermission=false, policy=nil, gidmatch=true" ,
fsGroupChangePolicy : nil ,
skipPermssion : false ,
gidOwnerMatch : true ,
} ,
2020-02-26 16:59:22 -05:00
{
description : "skippermission=false, policy=onrootmismatch, gidmatch=false" ,
fsGroupChangePolicy : & onrootMismatch ,
gidOwnerMatch : false ,
skipPermssion : false ,
} ,
{
description : "skippermission=false, policy=onrootmismatch, gidmatch=true, permmatch=false" ,
fsGroupChangePolicy : & onrootMismatch ,
gidOwnerMatch : true ,
permissionMatch : false ,
skipPermssion : false ,
} ,
{
description : "skippermission=false, policy=onrootmismatch, gidmatch=true, permmatch=true" ,
fsGroupChangePolicy : & onrootMismatch ,
gidOwnerMatch : true ,
permissionMatch : true ,
skipPermssion : false ,
} ,
{
description : "skippermission=false, policy=onrootmismatch, gidmatch=true, permmatch=true, sgidmatch=true" ,
fsGroupChangePolicy : & onrootMismatch ,
gidOwnerMatch : true ,
permissionMatch : true ,
sgidMatch : true ,
skipPermssion : true ,
} ,
}
for _ , test := range tests {
t . Run ( test . description , func ( t * testing . T ) {
tmpDir , err := utiltesting . MkTmpdir ( "volume_linux_test" )
if err != nil {
t . Fatalf ( "error creating temp dir: %v" , err )
}
2025-02-26 20:57:53 -05:00
defer func ( ) {
err := os . RemoveAll ( tmpDir )
if err != nil {
t . Fatalf ( "error removing tmpDir %s: %v" , tmpDir , err )
}
} ( )
2020-02-26 16:59:22 -05:00
info , err := os . Lstat ( tmpDir )
if err != nil {
t . Fatalf ( "error reading permission of tmpdir: %v" , err )
}
stat , ok := info . Sys ( ) . ( * syscall . Stat_t )
if ! ok || stat == nil {
t . Fatalf ( "error reading permission stats for tmpdir: %s" , tmpDir )
}
gid := stat . Gid
2025-02-26 20:57:53 -05:00
var expectedGID int64
2020-02-26 16:59:22 -05:00
if test . gidOwnerMatch {
2025-02-26 20:57:53 -05:00
expectedGID = int64 ( gid )
2020-02-26 16:59:22 -05:00
} else {
2025-02-26 20:57:53 -05:00
expectedGID = int64 ( gid + 3000 )
2020-02-26 16:59:22 -05:00
}
mask := rwMask
if test . permissionMatch {
mask |= execMask
}
2020-04-01 11:20:38 -04:00
if test . sgidMatch {
mask |= os . ModeSetgid
mask = info . Mode ( ) | mask
} else {
nosgidPerm := info . Mode ( ) &^ os . ModeSetgid
mask = nosgidPerm | mask
}
err = os . Chmod ( tmpDir , mask )
2020-02-26 16:59:22 -05:00
if err != nil {
t . Errorf ( "Chmod failed on %v: %v" , tmpDir , err )
}
mounter := & localFakeMounter { path : tmpDir }
2025-02-26 20:57:53 -05:00
ok = skipPermissionChange ( mounter , tmpDir , & expectedGID , test . fsGroupChangePolicy )
2020-02-26 16:59:22 -05:00
if ok != test . skipPermssion {
t . Errorf ( "for %s expected skipPermission to be %v got %v" , test . description , test . skipPermssion , ok )
}
} )
}
2020-02-28 12:23:31 -05:00
}
2020-11-11 08:29:32 -05:00
func TestSetVolumeOwnershipMode ( t * testing . T ) {
2020-02-28 12:23:31 -05:00
always := v1 . FSGroupChangeAlways
onrootMismatch := v1 . FSGroupChangeOnRootMismatch
expectedMask := rwMask | os . ModeSetgid | execMask
tests := [ ] struct {
description string
fsGroupChangePolicy * v1 . PodFSGroupChangePolicy
setupFunc func ( path string ) error
assertFunc func ( path string ) error
} {
{
description : "featuregate=on, fsgroupchangepolicy=always" ,
fsGroupChangePolicy : & always ,
setupFunc : func ( path string ) error {
info , err := os . Lstat ( path )
if err != nil {
return err
}
// change mode of root folder to be right
err = os . Chmod ( path , info . Mode ( ) | expectedMask )
if err != nil {
return err
}
// create a subdirectory with invalid permissions
rogueDir := filepath . Join ( path , "roguedir" )
2020-04-01 11:20:38 -04:00
nosgidPerm := info . Mode ( ) &^ os . ModeSetgid
err = os . Mkdir ( rogueDir , nosgidPerm )
2020-02-28 12:23:31 -05:00
if err != nil {
return err
}
return nil
} ,
assertFunc : func ( path string ) error {
rogueDir := filepath . Join ( path , "roguedir" )
hasCorrectPermissions := verifyDirectoryPermission ( rogueDir , false /*readOnly*/ )
if ! hasCorrectPermissions {
return fmt . Errorf ( "invalid permissions on %s" , rogueDir )
}
return nil
} ,
} ,
{
description : "featuregate=on, fsgroupchangepolicy=onrootmismatch,rootdir=validperm" ,
fsGroupChangePolicy : & onrootMismatch ,
setupFunc : func ( path string ) error {
info , err := os . Lstat ( path )
if err != nil {
return err
}
// change mode of root folder to be right
err = os . Chmod ( path , info . Mode ( ) | expectedMask )
if err != nil {
return err
}
// create a subdirectory with invalid permissions
rogueDir := filepath . Join ( path , "roguedir" )
err = os . Mkdir ( rogueDir , rwMask )
if err != nil {
return err
}
return nil
} ,
assertFunc : func ( path string ) error {
rogueDir := filepath . Join ( path , "roguedir" )
hasCorrectPermissions := verifyDirectoryPermission ( rogueDir , false /*readOnly*/ )
if hasCorrectPermissions {
return fmt . Errorf ( "invalid permissions on %s" , rogueDir )
}
return nil
} ,
} ,
{
description : "featuregate=on, fsgroupchangepolicy=onrootmismatch,rootdir=invalidperm" ,
fsGroupChangePolicy : & onrootMismatch ,
setupFunc : func ( path string ) error {
// change mode of root folder to be right
err := os . Chmod ( path , 0770 )
if err != nil {
return err
}
// create a subdirectory with invalid permissions
rogueDir := filepath . Join ( path , "roguedir" )
err = os . Mkdir ( rogueDir , rwMask )
if err != nil {
return err
}
return nil
} ,
assertFunc : func ( path string ) error {
rogueDir := filepath . Join ( path , "roguedir" )
hasCorrectPermissions := verifyDirectoryPermission ( rogueDir , false /*readOnly*/ )
if ! hasCorrectPermissions {
return fmt . Errorf ( "invalid permissions on %s" , rogueDir )
}
return nil
} ,
} ,
}
for _ , test := range tests {
t . Run ( test . description , func ( t * testing . T ) {
tmpDir , err := utiltesting . MkTmpdir ( "volume_linux_ownership" )
if err != nil {
t . Fatalf ( "error creating temp dir: %v" , err )
}
2025-02-26 20:57:53 -05:00
defer func ( ) {
err := os . RemoveAll ( tmpDir )
if err != nil {
t . Fatalf ( "error removing tmpDir %s: %v" , tmpDir , err )
}
} ( )
2025-02-26 13:27:32 -05:00
2020-02-28 12:23:31 -05:00
info , err := os . Lstat ( tmpDir )
if err != nil {
t . Fatalf ( "error reading permission of tmpdir: %v" , err )
}
stat , ok := info . Sys ( ) . ( * syscall . Stat_t )
if ! ok || stat == nil {
t . Fatalf ( "error reading permission stats for tmpdir: %s" , tmpDir )
}
2025-02-26 20:57:53 -05:00
var expectedGID = int64 ( stat . Gid )
2020-02-28 12:23:31 -05:00
err = test . setupFunc ( tmpDir )
if err != nil {
t . Errorf ( "for %s error running setup with: %v" , test . description , err )
}
2023-03-30 16:27:58 -04:00
mounter := & localFakeMounter { path : "FAKE_DIR_DOESNT_EXIST" } // SetVolumeOwnership() must rely on tmpDir
2025-02-26 20:57:53 -05:00
ownershipChanger := NewVolumeOwnership ( mounter , tmpDir , & expectedGID , test . fsGroupChangePolicy , nil )
2025-02-24 12:48:38 -05:00
err = ownershipChanger . ChangePermissions ( )
2020-02-28 12:23:31 -05:00
if err != nil {
t . Errorf ( "for %s error changing ownership with: %v" , test . description , err )
}
err = test . assertFunc ( tmpDir )
if err != nil {
t . Errorf ( "for %s error verifying permissions with: %v" , test . description , err )
}
} )
}
}
2025-02-26 13:27:32 -05:00
func TestProgressTracking ( t * testing . T ) {
alwaysApplyPolicy := v1 . FSGroupChangeAlways
var expectedGID int64 = 9999
2025-03-06 10:19:21 -05:00
// capture original variable
originalfilePermissionChangeFunc := filePermissionChangeFunc
originalProgressReportDuration := progressReportDuration
originalfirstEventReportDuration := firstEventReportDuration
2025-02-26 13:27:32 -05:00
var permissionSleepDuration = 5 * time . Millisecond
2025-03-06 10:19:21 -05:00
// Override how often progress is reported
2025-02-26 13:27:32 -05:00
progressReportDuration = 200 * time . Millisecond
2025-03-06 10:19:21 -05:00
// Override when first event about progress is reported
2025-02-26 13:27:32 -05:00
firstEventReportDuration = 50 * time . Millisecond
2025-03-06 10:19:21 -05:00
// Override how permission change is applied, so as to artificially slow
// permission change
filePermissionChangeFunc = func ( filename string , fsGroup * int64 , readonly bool , info os . FileInfo ) error {
2025-02-26 13:27:32 -05:00
time . Sleep ( permissionSleepDuration )
2025-03-06 10:19:21 -05:00
originalfilePermissionChangeFunc ( filename , fsGroup , readonly , info )
2025-02-26 13:27:32 -05:00
return nil
}
2025-03-06 10:19:21 -05:00
t . Cleanup ( func ( ) {
filePermissionChangeFunc = originalfilePermissionChangeFunc
progressReportDuration = originalProgressReportDuration
firstEventReportDuration = originalfirstEventReportDuration
} )
2025-02-26 13:27:32 -05:00
pod := & v1 . Pod {
ObjectMeta : metav1 . ObjectMeta {
Name : "pod1" ,
UID : "pod1uid" ,
} ,
Spec : v1 . PodSpec {
Volumes : [ ] v1 . Volume {
{
Name : "volume-name" ,
VolumeSource : v1 . VolumeSource {
GCEPersistentDisk : & v1 . GCEPersistentDiskVolumeSource {
PDName : "fake-device1" ,
} ,
} ,
} ,
} ,
} ,
}
tests := [ ] struct {
name string
filePermissionChangeTimeDuration time . Duration
totalWaitTime time . Duration
currentPod * v1 . Pod
expectedEvents [ ] string
} {
{
name : "When permission change finishes quickly, no events should be logged" ,
filePermissionChangeTimeDuration : 30 * time . Millisecond ,
totalWaitTime : 1 * time . Second ,
currentPod : pod ,
expectedEvents : [ ] string { } ,
} ,
{
name : "When no pod is specified, no events should be logged" ,
filePermissionChangeTimeDuration : 300 * time . Millisecond ,
totalWaitTime : 1 * time . Second ,
currentPod : nil ,
expectedEvents : [ ] string { } ,
} ,
{
name : "When permission change takes loo long and pod is specified" ,
filePermissionChangeTimeDuration : 300 * time . Millisecond ,
totalWaitTime : 1 * time . Second ,
currentPod : pod ,
expectedEvents : [ ] string {
"Warning VolumePermissionChangeInProgress Setting volume ownership for pod1uid/volumes/faketype is taking longer than expected, consider using OnRootMismatch - https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#configure-volume-permission-and-ownership-change-policy-for-pods" ,
"Warning VolumePermissionChangeInProgress Setting volume ownership for pod1uid/volumes/faketype, processed 1 files." ,
} ,
} ,
}
for i := range tests {
tc := tests [ i ]
t . Run ( tc . name , func ( t * testing . T ) {
tmpDir , err := utiltesting . MkTmpdir ( "volume_linux_ownership" )
if err != nil {
t . Fatalf ( "error creating temp dir: %v" , err )
}
podUID := "placeholder"
if tc . currentPod != nil {
podUID = string ( tc . currentPod . UID )
}
volumePath := filepath . Join ( tmpDir , podUID , "volumes" , "faketype" )
err = os . MkdirAll ( volumePath , 0770 )
if err != nil {
t . Fatalf ( "error creating volumePath %s: %v" , volumePath , err )
}
defer func ( ) {
err := os . RemoveAll ( tmpDir )
if err != nil {
t . Fatalf ( "error removing tmpDir %s: %v" , tmpDir , err )
}
} ( )
mounter := & localFakeMounter { path : "FAKE_DIR_DOESNT_EXIST" } // SetVolumeOwnership() must rely on tmpDir
fakeRecorder := record . NewFakeRecorder ( 100 )
recordedEvents := [ ] string { }
// Set how long file permission change takes
permissionSleepDuration = tc . filePermissionChangeTimeDuration
ownershipChanger := NewVolumeOwnership ( mounter , volumePath , & expectedGID , & alwaysApplyPolicy , nil )
if tc . currentPod != nil {
ownershipChanger . AddProgressNotifier ( tc . currentPod , fakeRecorder )
}
err = ownershipChanger . ChangePermissions ( )
if err != nil {
t . Errorf ( "unexpected error: %+v" , err )
}
time . Sleep ( tc . totalWaitTime )
actualEventCount := len ( fakeRecorder . Events )
if len ( tc . expectedEvents ) == 0 && actualEventCount != len ( tc . expectedEvents ) {
t . Errorf ( "expected 0 events got %d" , actualEventCount )
}
for range actualEventCount {
event := <- fakeRecorder . Events
recordedEvents = append ( recordedEvents , event )
}
for i , event := range tc . expectedEvents {
if event != recordedEvents [ i ] {
t . Errorf ( "expected event %d to be %s, got: %s" , i , event , recordedEvents [ i ] )
}
}
} )
}
}
2020-02-28 12:23:31 -05:00
// verifyDirectoryPermission checks if given path has directory permissions
// that is expected by k8s. If returns true if it does otherwise false
func verifyDirectoryPermission ( path string , readonly bool ) bool {
info , err := os . Lstat ( path )
if err != nil {
return false
}
stat , ok := info . Sys ( ) . ( * syscall . Stat_t )
if ! ok || stat == nil {
return false
}
unixPerms := rwMask
if readonly {
unixPerms = roMask
}
2020-02-26 16:59:22 -05:00
2020-02-28 12:23:31 -05:00
unixPerms |= execMask
filePerm := info . Mode ( ) . Perm ( )
if ( unixPerms & filePerm == unixPerms ) && ( info . Mode ( ) & os . ModeSetgid != 0 ) {
return true
}
return false
2020-02-26 16:59:22 -05:00
}
2020-11-11 08:29:32 -05:00
func TestSetVolumeOwnershipOwner ( t * testing . T ) {
fsGroup := int64 ( 3000 )
2022-07-14 03:55:16 -04:00
currentUid := os . Geteuid ( )
2020-11-11 08:29:32 -05:00
if currentUid != 0 {
t . Skip ( "running as non-root" )
}
2025-02-26 20:57:53 -05:00
currentGID := os . Getgid ( )
2020-11-11 08:29:32 -05:00
tests := [ ] struct {
description string
fsGroup * int64
setupFunc func ( path string ) error
assertFunc func ( path string ) error
} {
{
description : "fsGroup=nil" ,
fsGroup : nil ,
setupFunc : func ( path string ) error {
filename := filepath . Join ( path , "file.txt" )
file , err := os . OpenFile ( filename , os . O_RDWR | os . O_CREATE , 0755 )
if err != nil {
return err
}
file . Close ( )
return nil
} ,
assertFunc : func ( path string ) error {
filename := filepath . Join ( path , "file.txt" )
2025-02-26 20:57:53 -05:00
if ! verifyFileOwner ( filename , currentUid , currentGID ) {
2020-11-11 08:29:32 -05:00
return fmt . Errorf ( "invalid owner on %s" , filename )
}
return nil
} ,
} ,
{
description : "*fsGroup=3000" ,
fsGroup : & fsGroup ,
setupFunc : func ( path string ) error {
filename := filepath . Join ( path , "file.txt" )
file , err := os . OpenFile ( filename , os . O_RDWR | os . O_CREATE , 0755 )
if err != nil {
return err
}
file . Close ( )
return nil
} ,
assertFunc : func ( path string ) error {
filename := filepath . Join ( path , "file.txt" )
if ! verifyFileOwner ( filename , currentUid , int ( fsGroup ) ) {
return fmt . Errorf ( "invalid owner on %s" , filename )
}
return nil
} ,
} ,
{
description : "symlink" ,
fsGroup : & fsGroup ,
setupFunc : func ( path string ) error {
filename := filepath . Join ( path , "file.txt" )
file , err := os . OpenFile ( filename , os . O_RDWR | os . O_CREATE , 0755 )
if err != nil {
return err
}
file . Close ( )
symname := filepath . Join ( path , "file_link.txt" )
err = os . Symlink ( filename , symname )
if err != nil {
return err
}
return nil
} ,
assertFunc : func ( path string ) error {
symname := filepath . Join ( path , "file_link.txt" )
if ! verifyFileOwner ( symname , currentUid , int ( fsGroup ) ) {
return fmt . Errorf ( "invalid owner on %s" , symname )
}
return nil
} ,
} ,
}
for _ , test := range tests {
t . Run ( test . description , func ( t * testing . T ) {
tmpDir , err := utiltesting . MkTmpdir ( "volume_linux_ownership" )
if err != nil {
t . Fatalf ( "error creating temp dir: %v" , err )
}
2025-02-26 20:57:53 -05:00
defer func ( ) {
err := os . RemoveAll ( tmpDir )
if err != nil {
t . Fatalf ( "error removing tmpDir %s: %v" , tmpDir , err )
}
} ( )
2020-11-11 08:29:32 -05:00
err = test . setupFunc ( tmpDir )
if err != nil {
t . Errorf ( "for %s error running setup with: %v" , test . description , err )
}
mounter := & localFakeMounter { path : tmpDir }
always := v1 . FSGroupChangeAlways
2025-02-24 12:48:38 -05:00
ownershipChanger := NewVolumeOwnership ( mounter , tmpDir , test . fsGroup , & always , nil )
err = ownershipChanger . ChangePermissions ( )
2020-11-11 08:29:32 -05:00
if err != nil {
t . Errorf ( "for %s error changing ownership with: %v" , test . description , err )
}
err = test . assertFunc ( tmpDir )
if err != nil {
t . Errorf ( "for %s error verifying permissions with: %v" , test . description , err )
}
} )
}
}
// verifyFileOwner checks if given path is owned by uid and gid.
// It returns true if it is otherwise false.
func verifyFileOwner ( path string , uid , gid int ) bool {
info , err := os . Lstat ( path )
if err != nil {
return false
}
stat , ok := info . Sys ( ) . ( * syscall . Stat_t )
if ! ok || stat == nil {
return false
}
if int ( stat . Uid ) != uid || int ( stat . Gid ) != gid {
return false
}
return true
}