Repository: grpc/grpc-go
File: credentials/tls/certprovider/pemfile/watcher.go (Go)

Code before:
	// Optional. If this is set, KeyFile must also be set.
	CertFile string
	// KeyFile is the file that holds identity private key.
	// Optional. If this is set, CertFile must also be set.
	KeyFile string
	// RootFile is the file that holds trusted root certificate(s).
	// Optional.
	RootFile string
	// SPIFFEBundleMapFile is the file that holds the spiffe bundle map.
	// If a given provider configures both the RootFile and the
	// SPIFFEBundleMapFile, the SPIFFEBundleMapFile will be preferred.
	// Optional.
	SPIFFEBundleMapFile string
	// RefreshDuration is the amount of time the plugin waits before checking
	// for updates in the specified files.
	// Optional. If not set, a default value (1 hour) will be used.
	RefreshDuration time.Duration
}

func (o Options) canonical() []byte {
	return []byte(fmt.Sprintf("%s:%s:%s:%s:%s", o.CertFile, o.KeyFile, o.RootFile, o.SPIFFEBundleMapFile, o.RefreshDuration))
}

func (o Options) validate() error {
	// Guard against SPIFFE bundle map usage
	if os.Getenv(spiffeEnabledEnvVar) != "true" {
		o.SPIFFEBundleMapFile = ""
	}
	if o.CertFile == "" && o.KeyFile == "" && o.RootFile == "" && o.SPIFFEBundleMapFile == "" {
		return fmt.Errorf("pemfile: at least one credential file needs to be specified")
	}
	if keySpecified, certSpecified := o.KeyFile != "", o.CertFile != ""; keySpecified != certSpecified {
		return fmt.Errorf("pemfile: private key file and identity cert file should be both specified or not specified")
	}
	// C-core has a limitation that they cannot verify that a certificate file
	// matches a key file. So, the only way to get around this is to make sure
	// that both files are in the same directory and that they do an atomic
	// read. Even though Java/Go do not have this limitation, we want the
	// overall plugin behavior to be consistent across languages.
	if certDir, keyDir := filepath.Dir(o.CertFile), filepath.Dir(o.KeyFile); certDir != keyDir {
		return errors.New("pemfile: certificate and key file must be in the same directory")
	}
	return nil
}

// NewProvider returns a new certificate provider plugin that is configured to
// watch the PEM files specified in the passed in options.
func NewProvider(o Options) (certprovider.Provider, error) {
	if err := o.validate(); err != nil {
		return nil, err
	}

Review comment (suggestion):
[internal](https://github.com/grpc/grpc-go/tree/6995ef2ab61c192a6a043ce4efe7f49a101e4c9f/internal)/[envconfig](https://github.com/grpc/grpc-go/tree/6995ef2ab61c192a6a043ce4efe7f49a101e4c9f/internal/envconfig)/[xds.go](https://github.com/grpc/grpc-go/blob/6995ef2ab61c192a6a043ce4efe7f49a101e4c9f/internal/envconfig/xds.go)
This might be a better place to get the value from env config. Then you might also not need to get the data from env variable twice seperately.
We can also set the default value to false initially and then default to true at a later stage.

Code after:
	CertFile string
	// KeyFile is the file that holds identity private key.
	// Optional. If this is set, CertFile must also be set.
	KeyFile string
	// RootFile is the file that holds trusted root certificate(s).
	// Optional.
	RootFile string
	// SPIFFEBundleMapFile is the file that holds the spiffe bundle map.
	// If a given provider configures both the RootFile and the
	// SPIFFEBundleMapFile, the SPIFFEBundleMapFile will be preferred.
	// Optional.
	SPIFFEBundleMapFile string
	// RefreshDuration is the amount of time the plugin waits before checking
	// for updates in the specified files.
	// Optional. If not set, a default value (1 hour) will be used.
	RefreshDuration time.Duration
}

func (o Options) canonical() []byte {
	return []byte(fmt.Sprintf("%s:%s:%s:%s:%s", o.CertFile, o.KeyFile, o.RootFile, o.SPIFFEBundleMapFile, o.RefreshDuration))
}

func (o Options) validate() error {
	if o.CertFile == "" && o.KeyFile == "" && o.RootFile == "" && o.SPIFFEBundleMapFile == "" {
		return fmt.Errorf("pemfile: at least one credential file needs to be specified")
	}
	if keySpecified, certSpecified := o.KeyFile != "", o.CertFile != ""; keySpecified != certSpecified {
		return fmt.Errorf("pemfile: private key file and identity cert file should be both specified or not specified")
	}
	// C-core has a limitation that they cannot verify that a certificate file
	// matches a key file. So, the only way to get around this is to make sure
	// that both files are in the same directory and that they do an atomic
	// read. Even though Java/Go do not have this limitation, we want the
	// overall plugin behavior to be consistent across languages.
	if certDir, keyDir := filepath.Dir(o.CertFile), filepath.Dir(o.KeyFile); certDir != keyDir {
		return errors.New("pemfile: certificate and key file must be in the same directory")
	}
	return nil
}

// NewProvider returns a new certificate provider plugin that is configured to
// watch the PEM files specified in the passed in options.
func NewProvider(o Options) (certprovider.Provider, error) {
	if err := o.validate(); err != nil {
		return nil, err
	}
	return newProvider(o), nil
}

// newProvider is used to create a new certificate provider plugin after
// validating the options, and hence does not return an error.